programing

Google Firestore:속성 값의 하위 문자열에 대한 쿼리(텍스트 검색)

i4 2023. 6. 5. 23:38
반응형

Google Firestore:속성 값의 하위 문자열에 대한 쿼리(텍스트 검색)

간단한 검색 필드를 추가하려고 하는데, 다음과 같은 것을 사용하고 싶습니다.

collectionRef.where('name', 'contains', 'searchTerm')

사용해 보았습니다.where('name', '==', '%searchTerm%')하지만 아무것도 돌려주지 않았습니다.

저는 @Kuba의 답변에 동의하지만, 여전히 접두사로 검색하기 위해 완벽하게 작동하려면 작은 변화를 추가해야 합니다.여기서 나에게 효과가 있었던 것.

이으로시는레검색용으로 검색의 queryText

collectionRef
    .where('name', '>=', queryText)
    .where('name', '<=', queryText+ '\uf8ff')

캐터릭.\uf8ff쿼리에 사용되는 코드는 유니코드 범위에서 매우 높은 코드 포인트입니다(Private Usage Area(PUA) 코드임).는 유코드에대의일반문으로 합니다.queryText.

전체 텍스트 검색, 관련 검색 및 Trigram 검색!

업데이트 - 2/17/21 - 몇 가지 새로운 전체 텍스트 검색 옵션을 만들었습니다.

코드를 참조하십시오.자세한 내용은 빌드하십시오.


참고로, dgraph는 이제 실시간으로 웹 소켓을 가지고 있습니다.와우, 그것이 오는 것을 본 적이 없어요, 정말 멋진 일이에요!클라우드 Dgraph - 놀랍습니다!


--원래 우편물--

몇 가지 참고 사항:

1.) \uf8ff와 동일한 방식으로 작동합니다.~

2.) where 절 또는 start end 절을 사용할 수 있습니다.

ref.orderBy('title').startAt(term).endAt(term + '~');

와 정확히 같습니다.

ref.where('title', '>=', term).where('title', '<=', term + '~');

3.) 아니요, 반대로 하면 작동하지 않습니다.startAt()그리고.endAt()그러나 모든 조합에서 반대로 두 번째 검색 필드를 만들고 결과를 결합하여 동일한 결과를 얻을 수 있습니다.

예:먼저 필드를 만들 때 필드의 반대 버전을 저장해야 합니다.이와 같은 것:

// collection
const postRef = db.collection('posts')

async function searchTitle(term) {

  // reverse term
  const termR = term.split("").reverse().join("");

  // define queries
  const titles = postRef.orderBy('title').startAt(term).endAt(term + '~').get();
  const titlesR = postRef.orderBy('titleRev').startAt(termR).endAt(termR + '~').get();

  // get queries
  const [titleSnap, titlesRSnap] = await Promise.all([
    titles,
    titlesR
  ]);
  return (titleSnap.docs).concat(titlesRSnap.docs);
}

이렇게 하면 문자열 필드의 마지막 문자와 첫 번째 문자를 검색할 수 있습니다. 중간 문자나 문자 그룹을 임의로 검색할 수 없습니다.이것은 원하는 결과에 더 가깝습니다.하지만, 이것은 우리가 임의의 중간 글자나 단어를 원할 때 실제로 도움이 되지 않을 것입니다.또한 대소문자를 구분하지 않도록 소문자 또는 소문자 사본을 모두 저장해야 합니다.

4.) 단어가 몇 개밖에 없는 경우, 켄 탄의 메소드는 원하는 모든 것을 수행하거나 적어도 약간 수정한 후에 수행됩니다.그러나 텍스트 한 단락만으로 1MB 이상의 데이터를 기하급수적으로 생성할 수 있으며, 이는 소방서의 문서 크기 제한보다 큽니다(알고 있습니다, 테스트했습니다).

5.) 어레이 포함(또는 어떤 형태의 어레이)을\uf8ff트릭, 제한에 도달하지 않는 실행 가능한 검색이 있을 수 있습니다.저는 모든 조합을 시도했습니다. 심지어 지도를 사용하기도 했고, 시도하지도 않았습니다.이걸 알아내신 분은 여기에 올려주세요.

6.) 만약 당신이 ALGOLIA와 ELASTIC Search에서 벗어나야 한다면, 당신은 항상 Google Cloud에서 mySQL, postSQL 또는 neo4Js를 사용할 수 있습니다.셋 모두 설정이 쉽고 무료 계층이 있습니다.Create()에 데이터를 저장하는 클라우드 기능과 데이터를 검색하는 onCall() 기능이 있습니다.단순... 이쉬.그렇다면 mySQL로 전환하는 것이 어떻습니까?실시간 데이터는 물론!누군가 실시간 데이터를 위해 웹 양말로 DGraph를 작성할 때, 저를 포함시키세요!

Algolia와 Elastic Search는 검색 전용 dbs로 만들어졌으므로, 그렇게 빠른 것은 없습니다...하지만 당신이 대가를 치릅니다.구글, 왜 우리를 구글에서 멀어지게 하고, MongoDB noSQL을 팔로우하고 검색을 허용하지 않습니까?

연산자는, 되는 연산자는 런그연없습니입니다. 허용되는 연산자는==,<,<=,>,>=.

예를 들어, 다음 사이에 시작하는 모든 항목에 대해서는 접두사로만 필터링할 수 있습니다.bar그리고.foo사용할 수 있습니다.

collectionRef
    .where('name', '>=', 'bar')
    .where('name', '<=', 'foo')

이를 위해 알골리아나 엘라스틱서치 같은 외부 서비스를 이용할 수 있습니다.

Kuba의 대답은 제한에 관한 한 사실이지만, 집합과 같은 구조로 부분적으로 이를 에뮬레이트할 수 있습니다.

{
  'terms': {
    'reebok': true,
    'mens': true,
    'tennis': true,
    'racket': true
  }
}

이제 쿼리할 수 있습니다.

collectionRef.where('terms.tennis', '==', true)

Firestore는 모든 필드에 대한 인덱스를 자동으로 만들기 때문에 이 작업이 가능합니다.안타깝게도 Firestore는 복합 인덱스를 자동으로 생성하지 않기 때문에 복합 쿼리에 대해 직접적으로 작동하지 않습니다.

단어의 조합을 저장하여 이 문제를 해결할 수 있지만 이 문제는 빠르게 해결됩니다.

아웃보드 전체 텍스트 검색을 사용하는 것이 더 나을 수도 있습니다.

Firebase는 문자열 내에서 용어 검색을 명시적으로 지원하지 않지만,

Firebase는 (현재) 귀하의 사례와 다른 많은 사례에 대해 해결할 수 있는 다음을 지원합니다.

, 은 2018년 8월 8일을 지원합니다.array-contains다음을 참조하십시오. https://firebase.googleblog.com/2018/08/better-arrays-in-cloud-firestore.html

이제 모든 주요 용어를 배열로 필드로 설정한 다음 'X'가 포함된 배열이 있는 모든 문서를 쿼리할 수 있습니다.논리적 AND를 사용하여 추가 쿼리를 비교할 수 있습니다.(이는 Firebase가 현재 여러 어레이에 대한 복합 쿼리를 기본적으로 지원하지 않으므로 'AND' 정렬 쿼리는 클라이언트 측에서 수행해야 하기 때문입니다.)

이 스타일로 배열을 사용하면 동시 쓰기에 최적화되어 유용합니다!배치 요청을 지원하는지 테스트하지 않았지만(의사가 말하지 않음) 공식 솔루션이기 때문에 지원할 것이라고 장담합니다.


용도:

collection("collectionPath").
    where("searchTermsArray", "array-contains", "term").get()

Firestore 문서에 따르면 Cloud Firestore는 기본 인덱싱 또는 문서의 텍스트 필드 검색을 지원하지 않습니다.또한 클라이언트 측 필드를 검색하기 위해 전체 컬렉션을 다운로드하는 것은 실용적이지 않습니다.

Algolia Elastic Search와 같은 타사 검색 솔루션이 권장됩니다.

Firebase는 곧 "string-contains"를 제공하여 문자열의 인덱스 [i] startAt를 캡처할 수 있을 것입니다.하지만 저는 웹을 조사했고 다른 사람이 당신의 데이터를 이렇게 설정하는 방법을 생각해 낸 것을 발견했습니다.

state = { title: "Knitting" };
// ...
const c = this.state.title.toLowerCase();

var array = [];
for (let i = 1; i < c.length + 1; i++) {
  array.push(c.substring(0, i));
}

firebase
  .firestore()
  .collection("clubs")
  .doc(documentId)
  .update({
    title: this.state.title,
    titleAsArray: array
  });

여기에 이미지 설명 입력

이렇게 묻습니다.

firebase.firestore()
    .collection("clubs")
    .where(
        "titleAsArray",
        "array-contains",
        this.state.userQuery.toLowerCase()
    )

오늘(18-Aug-2020) 현재 전문가들이 질문에 대한 답으로 제시한 해결 방법은 기본적으로 3가지입니다.

다 먹어봤어요.각각의 경험을 문서화하는 것이 유용할 것이라고 생각했습니다.

방법-A: 사용: (dbField ">=" searchString) & (dbField "<=" searchString + "\uf8ff")

@Kuba & @Ankit Prajapati가 제안합니다.

.where("dbField1", ">=", searchString)
.where("dbField1", "<=", searchString + "\uf8ff");

A.1 Firestore 쿼리는 단일 필드에서만 범위 필터(>, >=, <=)를 수행할 수 있습니다.여러 필드에 범위 필터가 있는 쿼리는 지원되지 않습니다.이 방법을 사용하면 db의 다른 필드(예: 날짜 필드)에 범위 연산자를 둘 수 없습니다.

A.2. 이 방법은 여러 필드에서 동시에 검색할 수 없습니다.예를 들어 검색 문자열이 필드(이름, 메모 및 주소)에 있는지 확인할 수 없습니다.

Method-B: 맵의 각 항목에 대해 "true"가 있는 검색 문자열의 MAP 사용 및 쿼리에서 "==" 연산자 사용

@길 길버트가 제안함

document1 = {
  'searchKeywordsMap': {
    'Jam': true,
    'Butter': true,
    'Muhamed': true,
    'Green District': true,
    'Muhamed, Green District': true,
  }
}

.where(`searchKeywordsMap.${searchString}`, "==", true);

B.1 분명히, 이 방법은 데이터가 DB에 저장될 때마다 추가 처리가 필요하며, 더 중요한 것은 검색 문자열의 맵을 저장할 추가 공간이 필요하다는 것입니다.

B.2 Firestore 쿼리에 위와 같은 단일 조건이 있는 경우 인덱스를 미리 생성할 필요가 없습니다.이 경우에는 이 솔루션이 잘 작동할 것입니다.

B.3 그러나 쿼리에 다른 조건(예: 상태 === "활성")이 있는 경우 사용자가 입력하는 각 "검색 문자열"에 대한 인덱스가 필요한 것으로 보입니다.즉, 사용자가 "Jam"을 검색하고 다른 사용자가 "Butter"를 검색할 경우, "Jam" 문자열에 대한 인덱스를 미리 생성하고 "Butter"에 대한 인덱스를 생성해야 합니다.가능한 모든 사용자의 검색 문자열을 예측할 수 없는 경우, 쿼리에 다른 조건이 있는 경우 이는 작동하지 않습니다!

.where(searchKeywordsMap["Jam"], "==", true); // requires an index on searchKeywordsMap["Jam"]
.where("status", "==", "active");

**Method-C: 검색 문자열의 ARARY 사용 및 "array-contains" 연산자

@Albert Renshaw가 제안하고 @Nick Carducci가 시연했습니다.

document1 = {
  'searchKeywordsArray': [
    'Jam',
    'Butter',
    'Muhamed',
    'Green District',
    'Muhamed, Green District',
  ]
}

.where("searchKeywordsArray", "array-contains", searchString); 

C.1 Method-B와 유사하게, 이 방법은 데이터가 DB에 저장될 때마다 추가 처리가 필요하며, 더 중요한 것은 검색 문자열 배열을 저장하기 위한 추가 공간이 필요하다는 것입니다.

C.2 Firestore 쿼리는 복합 쿼리에 최대 하나의 "array-contains" 또는 "array-contains-any" 절을 포함할 수 있습니다.

일반적인 제한 사항:

  1. 이러한 솔루션 중 일부 문자열 검색을 지원하는 솔루션은 없습니다.예를 들어, db 필드에 "1 Peter St, Green District"가 포함된 경우 "strict" 문자열을 검색할 수 없습니다.
  2. 예상되는 검색 문자열의 가능한 모든 조합을 포함하는 것은 거의 불가능합니다.예를 들어, DB 필드에 "1 Mohamed St, Green District"가 포함된 경우, DB 필드에 사용된 순서와 다른 순서로 단어를 가진 문자열인 "Green Mohamed"를 검색할 수 없습니다.

모든 것에 맞는 솔루션은 없습니다.각 해결 방법에는 제한이 있습니다.위의 정보가 이러한 해결 방법 사이의 선택 과정에서 도움이 되기를 바랍니다.

Firestore 쿼리 조건 목록은 https://firebase.google.com/docs/firestore/query-data/queries 설명서를 참조하십시오.

@Jonathan이 제안한 https://fireblog.io/blog/post/firestore-full-text-search, 사용해 본 적이 없습니다.

답변이 늦었지만 여전히 답변을 찾고 있는 사람들을 위해, 우리가 사용자 모음을 가지고 있고 모음의 각 문서에 "사용자 이름" 필드가 있다고 가정하자, 만약 사용자 이름이 "알"로 시작하는 문서를 찾고 싶다면, 우리는 다음과 같은 것을 할 수 있습니다.

FirebaseFirestore.getInstance()
    .collection("users")
    .whereGreaterThanOrEqualTo("username", "al")

저는 방금 이 문제를 겪었고 꽤 간단한 해결책을 생각해냈습니다.

String search = "ca";
Firestore.instance.collection("categories").orderBy("name").where("name",isGreaterThanOrEqualTo: search).where("name",isLessThanOrEqualTo: search+"z")

The is GreaterThanOrEqualTo를 사용하면 검색 시작을 필터링하고 isLess 끝에 "z"를 추가할 수 있습니다.ThanOrEqualTo 검색을 다음 문서로 롤오버하지 않도록 제한합니다.

저는 조나단이 말한 것처럼 트라이그램을 사용했습니다.

트램은 검색을 돕기 위해 데이터베이스에 저장된 3개의 문자 그룹입니다. 그래서 만약 내가 사용자들의 데이터를 가지고 있고 나는 도널드 트럼프에 대해 '트럼'을 쿼리하고 싶다고 말하면 나는 그것을 이런 식으로 저장해야 합니다.

여기에 이미지 설명 입력

그리고 이 방법을 기억해 내려고요.

 onPressed: () {
      //LET SAY YOU TYPE FOR 'tru' for trump
      List<String> search = ['tru', 'rum'];
      Future<QuerySnapshot> inst = FirebaseFirestore.instance
          .collection("users")
          .where('trigram', arrayContainsAny: search)
          .get();
      print('result=');
      inst.then((value) {
        for (var i in value.docs) {
          print(i.data()['name']);
        }
      });

어떤 일이 있어도 정확한 결과를 얻을 것입니다.

여기에 이미지 설명 입력

EDIT 05/2021:

Google Firebase에는 이제 Algolia로 검색을 구현할 수 있는 확장 기능이 있습니다.Algolia는 광범위한 기능 목록을 가진 전체 텍스트 검색 플랫폼입니다.Firebase에 "Blaze" 계획이 있어야 하며 Algolia 쿼리와 관련된 수수료가 있지만 프로덕션 애플리케이션에 대한 권장 접근 방식입니다.무료 기본 검색을 선호한다면 아래의 제 원래 답변을 참조하십시오.

https://firebase.google.com/products/extensions/firestore-algolia-search https://www.algolia.com

원답:

선택한 답변은 정확한 검색에만 사용할 수 있으며 자연스러운 사용자 검색 동작이 아닙니다("오늘 사과 먹기"에서 "apple"을 검색하면 작동하지 않습니다).

위의 댄 페인의 답변은 더 높은 순위가 되어야 한다고 생각합니다.검색 중인 문자열 데이터가 짧으면 문서의 배열에 문자열의 모든 하위 문자열을 저장한 다음 Firebase의 array_contains 쿼리를 사용하여 배열을 검색할 수 있습니다.Firebase Documents는 1MiB(1,048,576바이트)(Firebase Quotas and Limits)로 제한되며, 이는 문서에 저장된 약 100만 문자(1문자 ~= 1바이트)입니다.서브스트링을 저장하는 것은 당신의 문서가 100만 마크에 가깝지 않은 한 괜찮습니다.

사용자 이름 검색 예제:

1단계: 프로젝트에 다음 문자열 확장자를 추가합니다.이렇게 하면 문자열을 쉽게 하위 문자열로 분할할 수 있습니다.(여기서 이걸 찾았습니다.)

extension String {

var length: Int {
    return count
}

subscript (i: Int) -> String {
    return self[i ..< i + 1]
}

func substring(fromIndex: Int) -> String {
    return self[min(fromIndex, length) ..< length]
}

func substring(toIndex: Int) -> String {
    return self[0 ..< max(0, toIndex)]
}

subscript (r: Range<Int>) -> String {
    let range = Range(uncheckedBounds: (lower: max(0, min(length, r.lowerBound)),
                                        upper: min(length, max(0, r.upperBound))))
    let start = index(startIndex, offsetBy: range.lowerBound)
    let end = index(start, offsetBy: range.upperBound - range.lowerBound)
    return String(self[start ..< end])
}

2단계: 사용자 이름을 저장할 때 이 기능의 결과도 동일한 문서에 배열로 저장합니다.이렇게 하면 원본 텍스트의 모든 변형이 생성되고 배열에 저장됩니다.예를 들어, 텍스트 입력 "Apple"은 ["a", "p", "p", "l", "e", "ap", "pp", "pl", "le", "app", "app", "ppl", "app", "app", "app" 등의 배열을 생성합니다.모든 결과를 원하는 경우 maximumStringSize를 0으로 유지할 수 있지만, 긴 텍스트가 있는 경우에는 문서 크기가 너무 커지기 전에 캡핑하는 것이 좋습니다. 약 15개 정도면 좋습니다(대부분의 사람들은 긴 구문을 검색하지 않습니다).

func createSubstringArray(forText text: String, maximumStringSize: Int?) -> [String] {
    
    var substringArray = [String]()
    var characterCounter = 1
    let textLowercased = text.lowercased()
    
    let characterCount = text.count
    for _ in 0...characterCount {
        for x in 0...characterCount {
            let lastCharacter = x + characterCounter
            if lastCharacter <= characterCount {
                let substring = textLowercased[x..<lastCharacter]
                substringArray.append(substring)
            }
        }
        characterCounter += 1
        
        if let max = maximumStringSize, characterCounter > max {
            break
        }
    }
    
    print(substringArray)
    return substringArray
}

3단계: Firebase의 array_contains 함수를 사용할 수 있습니다!

[yourDatabasePath].whereField([savedSubstringArray], arrayContains: searchText).getDocuments....

Firestore에서 이를 수행하는 가장 좋은 방법은 모든 서브스트링을 배열에 넣고 array_contains 쿼리를 수행하는 것입니다.이렇게 하면 하위 문자열 일치를 수행할 수 있습니다.모든 서브스트링을 저장하기에는 약간 오버킬이지만 검색어가 짧다면 매우 합리적입니다.

Algolia와 같은 타사 서비스를 사용하지 않으려면 Firebase Cloud Functions가 좋은 대안입니다.입력 매개 변수를 수신하고 레코드 서버 측에서 처리한 다음 기준과 일치하는 것을 반환할 수 있는 함수를 만들 수 있습니다.

2023년 3월 현재 Firestore의 새로운 쿼리를 통해 접두사 검색이 대소문자를 구분한다는 문제를 제거할 수 있습니다.

query(
  collection(DB, 'some/collection'),
  or(
    // query as-is:
    and(
      where('name', '>=', queryString),
      where('name', '<=', queryString + '\uf8ff')
    ),
    // capitalize first letter:
    and(
      where('name', '>=', queryString.charAt(0).toUpperCase() + queryString.slice(1)),
      where('name', '<=', queryString.charAt(0).toUpperCase() + queryString.slice(1) + '\uf8ff')
    ),
    // lowercase:
    and(
      where('name', '>=', queryString.toLowerCase()),
      where('name', '<=', queryString.toLowerCase() + '\uf8ff')
    )
  )
);

이것은 저에게 완벽하게 효과가 있었지만 성능 문제를 일으킬 수 있습니다.

Firestore를 쿼리할 때 다음 작업을 수행합니다.

   Future<QuerySnapshot> searchResults = collectionRef
        .where('property', isGreaterThanOrEqualTo: searchQuery.toUpperCase())
        .getDocuments();

Future Builder에서 이 작업을 수행합니다.

    return FutureBuilder(
          future: searchResults,
          builder: (context, snapshot) {           
            List<Model> searchResults = [];
            snapshot.data.documents.forEach((doc) {
              Model model = Model.fromDocumet(doc);
              if (searchQuery.isNotEmpty &&
                  !model.property.toLowerCase().contains(searchQuery.toLowerCase())) {
                return;
              }

              searchResults.add(model);
            })
   };

다음 코드 스니펫은 사용자로부터 입력을 받아 입력된 것부터 시작하는 데이터를 가져옵니다.

표본 데이터:

Firebase Collection '사용자'에서

사용자 1: {name: 'Ali', 나이: 28,

사용자 2: {name: 'Khan', 나이: 30세,

사용자 3: {name: 'Hassan', 나이: 26,

사용자 4: {name: 'Adil', 나이: 32}

텍스트 입력:a

결과:

{name: 'Ali', 나이: 28세,

{name: 'Adil', 나이: 32}

let timer;

// method called onChangeText from TextInput

const textInputSearch = (text) => {

const inputStart = text.trim();
let lastLetterCode = inputStart.charCodeAt(inputStart.length-1);
lastLetterCode++;
const newLastLetter = String.fromCharCode(lastLetterCode);
const inputEnd = inputStart.slice(0,inputStart.length-1) + lastLetterCode;

clearTimeout(timer);

timer = setTimeout(() => {
    firestore().collection('Users')
        .where('name', '>=', inputStart)
        .where('name', '<', inputEnd)
        .limit(10)
        .get()
        .then(querySnapshot => {
            const users = [];
                querySnapshot.forEach(doc => {
                    users.push(doc.data());
                })
            setUsers(users); //  Setting Respective State
        });
    }, 1000);

};

2021년 업데이트

다른 답변에서 몇 가지를 가져왔습니다.여기에는 다음이 포함됩니다.

  • 분할을 사용한 다중 단어 검색(OR 역할)
  • 플랫을 사용한 다중 키 검색

대/소문자 구분에 약간 제한이 있지만 중복 속성을 대문자로 저장하면 이 문제를 해결할 수 있습니다. 예:query.toUpperCase() user.last_name_upper


// query: searchable terms as string

let users = await searchResults("Bob Dylan", 'users');

async function searchResults(query = null, collection = 'users', keys = ['last_name', 'first_name', 'email']) {

    let querySnapshot = { docs : [] };

    try {
        if (query) {
            let search = async (query)=> {
                let queryWords = query.trim().split(' ');
                return queryWords.map((queryWord) => keys.map(async (key) =>
                    await firebase
                        .firestore()
                        .collection(collection)
                        .where(key, '>=', queryWord)
                        .where(key, '<=', queryWord +  '\uf8ff')
                        .get())).flat();
            }

            let results = await search(query);

            await (await Promise.all(results)).forEach((search) => {
                querySnapshot.docs = querySnapshot.docs.concat(search.docs);
            });
        } else {
            // No query
            querySnapshot = await firebase
                .firestore()
                .collection(collection)
                // Pagination (optional)
                // .orderBy(sortField, sortOrder)
                // .startAfter(startAfter)
                // .limit(perPage)
                .get();
        }
    } catch(err) {
        console.log(err)
    }

    // Appends id and creates clean Array
    const items = [];
    querySnapshot.docs.forEach(doc => {
        let item = doc.data();
        item.id = doc.id;
        items.push(item);
    });

    // Filters duplicates
    return items.filter((v, i, a) => a.findIndex(t => (t.id === v.id)) === i);
}

참고: Firebase 호출 수는 쿼리 문자열의 단어 수 * 검색 중인 키 수와 같습니다.

@nicksarno와 동일하지만 확장이 필요 없는 더 세련된 코드를 사용합니다.

1단계

func getSubstrings(from string: String, maximumSubstringLenght: Int = .max) -> [Substring] {
    let string = string.lowercased()
    let stringLength = string.count
    let stringStartIndex = string.startIndex
    var substrings: [Substring] = []
    for lowBound in 0..<stringLength {
        for upBound in lowBound..<min(stringLength, lowBound+maximumSubstringLenght) {
            let lowIndex = string.index(stringStartIndex, offsetBy: lowBound)
            let upIndex = string.index(stringStartIndex, offsetBy: upBound)
            substrings.append(string[lowIndex...upIndex])
        }
    }
    return substrings
}

2단계

let name = "Lorenzo"
ref.setData(["name": name, "nameSubstrings": getSubstrings(from: name)])

3단계

Firestore.firestore().collection("Users")
  .whereField("nameSubstrings", arrayContains: searchText)
  .getDocuments...

이 함수 Firestore를 수 . 따라서 이 접근 방식에서는 입력 텍스트를 토큰화한 다음 선형 해시 함수를 선택하는 동안 입력 텍스트를 해시할 수 있습니다.h(x)하는 - if 다을만키는시 - 만약음.x < y < z then h(x) < h (y) < h(z)토큰화를 위해 문장에서 불필요한 단어를 제거할 수 있는 기능의 콜드 스타트 시간을 낮게 유지하기 위해 일부 경량 NLP 라이브러리를 선택할 수 있습니다.그런 다음 Firestore에서 보다 작은 연산자와 보다 큰 연산자를 사용하여 쿼리를 실행할 수 있습니다.데이터를 저장하는 동안에는 텍스트를 저장하기 전에 텍스트를 해시해야 하며 일반 텍스트를 변경하는 경우 해시 값도 변경되는 것처럼 일반 텍스트를 저장해야 합니다.

Typesense 서비스는 Firebase Cloud Firestore 데이터베이스에 대한 하위 문자열 검색을 제공합니다.

https://typesense.org/docs/guide/firebase-full-text-search.html

다음은 제 프로젝트에 관련된 유형 감각 통합 코드입니다.

lib/sys/typesense.dll

import 'dart:convert';

import 'package:flutter_instagram_clone/model/PostModel.dart';
import 'package:http/http.dart' as http;

class Typesense {
  static String baseUrl = 'http://typesense_server_ip:port/';
  static String apiKey = 'xxxxxxxx'; // your Typesense API key
  static String resource = 'collections/postData/documents/search';

  static Future<List<PostModel>> search(String searchKey, int page, {int contentType=-1}) async {
    if (searchKey.isEmpty) return [];

    List<PostModel> _results = [];

    var header = {'X-TYPESENSE-API-KEY': apiKey};
    String strSearchKey4Url = searchKey.replaceFirst('#', '%23').replaceAll(' ', '%20');
    String url = baseUrl +
        resource +
        '?q=${strSearchKey4Url}&query_by=postText&page=$page&sort_by=millisecondsTimestamp:desc&num_typos=0';
    if(contentType==0)
    {
      url += "&filter_by=isSelling:false";
    } else if(contentType == 1)
    {
      url += "&filter_by=isSelling:true";
    }

    var response = await http.get(Uri.parse(url), headers: header);

    var data = json.decode(response.body);
    for (var item in data['hits']) {
      PostModel _post = PostModel.fromTypeSenseJson(item['document']);

      if (searchKey.contains('#')) {
        if (_post.postText.toLowerCase().contains(searchKey.toLowerCase()))
          _results.add(_post);
      } else {
        _results.add(_post);
      }
    }

    print(_results.length);
    return _results;
  }

  static Future<List<PostModel>> getHubPosts(String searchKey, int page,
      {List<String>? authors, bool? isSelling}) async {
    List<PostModel> _results = [];

    var header = {'X-TYPESENSE-API-KEY': apiKey};

    String filter = "";
    if (authors != null || isSelling != null) {
      filter += "&filter_by=";

      if (isSelling != null) {
        filter += "isSelling:$isSelling";
        if (authors != null && authors.isNotEmpty) {
          filter += "&&";
        }
      }

      if (authors != null && authors.isNotEmpty) {
        filter += "authorID:$authors";
      }
    }

    String url = baseUrl +
        resource +
        '?q=${searchKey.replaceFirst('#', '%23')}&query_by=postText&page=$page&sort_by=millisecondsTimestamp:desc&num_typos=0$filter';

    var response = await http.get(Uri.parse(url), headers: header);

    var data = json.decode(response.body);
    for (var item in data['hits']) {
      PostModel _post = PostModel.fromTypeSenseJson(item['document']);
      _results.add(_post);
    }

    print(_results.length);

    return _results;
  }
}

lib/services/hubDetailsService.dart

import 'package:flutter/material.dart';
import 'package:flutter_instagram_clone/model/PostModel.dart';
import 'package:flutter_instagram_clone/utils/typesense.dart';

class HubDetailsService with ChangeNotifier {
  String searchKey = '';
  List<String>? authors;
  bool? isSelling;
  int nContentType=-1;


  bool isLoading = false;
  List<PostModel> hubResults = [];
  int _page = 1;
  bool isMore = true;
  bool noResult = false;

  Future initSearch() async {
    isLoading = true;
    isMore = true;
    noResult = false;
    hubResults = [];
    _page = 1;
    List<PostModel> _results = await Typesense.search(searchKey, _page, contentType: nContentType);
    for(var item in _results) {
      hubResults.add(item);
    }
    isLoading = false;
    if(_results.length < 10) isMore = false;
    if(_results.isEmpty) noResult = true;
    notifyListeners();
  }

  Future nextPage() async {
    if(!isMore) return;
    _page++;
    List<PostModel> _results = await Typesense.search(searchKey, _page);
    hubResults.addAll(_results);
    if(_results.isEmpty) {
      isMore = false;
    }
    notifyListeners();
  }

  Future refreshPage() async {
    isLoading = true;
    notifyListeners();
    await initSearch();
    isLoading = false;
    notifyListeners();
  }

  Future search(String _searchKey) async {
    isLoading = true;
    notifyListeners();
    searchKey = _searchKey;
    await initSearch();
    isLoading = false;
    notifyListeners();
  }
}

lib/ui/hub/hubDetailsScreen.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_instagram_clone/constants.dart';
import 'package:flutter_instagram_clone/main.dart';
import 'package:flutter_instagram_clone/model/MessageData.dart';
import 'package:flutter_instagram_clone/model/SocialReactionModel.dart';
import 'package:flutter_instagram_clone/model/User.dart';
import 'package:flutter_instagram_clone/model/hubModel.dart';
import 'package:flutter_instagram_clone/services/FirebaseHelper.dart';
import 'package:flutter_instagram_clone/services/HubService.dart';
import 'package:flutter_instagram_clone/services/helper.dart';
import 'package:flutter_instagram_clone/services/hubDetailsService.dart';
import 'package:flutter_instagram_clone/ui/fullScreenImageViewer/FullScreenImageViewer.dart';
import 'package:flutter_instagram_clone/ui/home/HomeScreen.dart';
import 'package:flutter_instagram_clone/ui/hub/editHubScreen.dart';
import 'package:provider/provider.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';

class HubDetailsScreen extends StatefulWidget {

  final HubModel hub;
  HubDetailsScreen(this.hub);

  @override
  _HubDetailsScreenState createState() => _HubDetailsScreenState();
}

class _HubDetailsScreenState extends State<HubDetailsScreen> {

  late HubDetailsService _service;
  List<SocialReactionModel?> _reactionsList = [];
  final fireStoreUtils = FireStoreUtils();
  late Future<List<SocialReactionModel>> _myReactions;
  final scrollController = ScrollController();
  bool _isSubLoading = false;


  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _service = Provider.of<HubDetailsService>(context, listen: false);
    print(_service.isLoading);
    init();
  }

  init() async {

    _service.searchKey = "";

    if(widget.hub.contentWords.length>0)
    {
      for(var item in widget.hub.contentWords) {
        _service.searchKey += item + " ";
      }
    }
    switch(widget.hub.contentType) {
      case 'All':
        break;
      case 'Marketplace':
        _service.isSelling = true;
        _service.nContentType = 1;
        break;
      case 'Post Only':
        _service.isSelling = false;
        _service.nContentType = 0;
        break;
      case 'Keywords':
        break;
    }

    for(var item in widget.hub.exceptWords) {
      if(item == 'Marketplace') {
        _service.isSelling = _service.isSelling != null?true:false;
      } else {
        _service.searchKey += "-" + item + "";
      }
    }

    if(widget.hub.fromUserType == 'Followers') {
      List<User> _followers = await fireStoreUtils.getFollowers(MyAppState.currentUser!.userID);

      _service.authors = [];
      for(var item in _followers)
        _service.authors!.add(item.userID);

    }

    if(widget.hub.fromUserType == 'Selected') {
      _service.authors = widget.hub.fromUserIds;
    }

    _service.initSearch();

    _myReactions = fireStoreUtils.getMyReactions()
      ..then((value) {
        _reactionsList.addAll(value);
      });

    scrollController.addListener(pagination);
  }


  void pagination(){
    if(scrollController.position.pixels ==
        scrollController.position.maxScrollExtent) {
      _service.nextPage();
    }
  }

  @override
  Widget build(BuildContext context) {

    Provider.of<HubDetailsService>(context);

    PageController _controller = PageController(
      initialPage: 0,
    );

    return Scaffold(
      backgroundColor: Colors.white,
      body: RefreshIndicator(
        onRefresh: () async {
          _service.refreshPage();
        },
        child: CustomScrollView(
          controller: scrollController,
          slivers: [
            SliverAppBar(
              centerTitle: false,
              expandedHeight: MediaQuery.of(context).size.height * 0.25,
              pinned: true,
              backgroundColor: Colors.white,
              title: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  InkWell(
                    onTap: (){
                      Navigator.pop(context);
                    },
                    child: Container(
                      width: 35, height: 35,
                      decoration: BoxDecoration(
                          color: Colors.white,
                          borderRadius: BorderRadius.circular(20)
                      ),
                      child: Center(
                        child: Icon(Icons.arrow_back),
                      ),
                    ),
                  ),

                  if(widget.hub.user.userID == MyAppState.currentUser!.userID)
                  InkWell(
                    onTap: () async {
                      var _hub = await push(context, EditHubScreen(widget.hub));

                      if(_hub != null) {
                        Navigator.pop(context, true);
                      }

                    },
                    child: Container(
                      width: 35, height: 35,
                      decoration: BoxDecoration(
                          color: Colors.white,
                          borderRadius: BorderRadius.circular(20)
                      ),
                      child: Center(
                        child: Icon(Icons.edit, color: Colors.black, size: 20,),
                      ),
                    ),
                  ),
                ],

              ),
              automaticallyImplyLeading: false,
              flexibleSpace: FlexibleSpaceBar(
                  collapseMode: CollapseMode.pin,
                  background: Container(color: Colors.grey,
                    child: Stack(
                      children: [
                        PageView.builder(
                            controller: _controller,
                            itemCount: widget.hub.medias.length,
                            itemBuilder: (context, index) {
                              Url postMedia = widget.hub.medias[index];
                              return GestureDetector(
                                  onTap: () => push(
                                      context,
                                      FullScreenImageViewer(
                                          imageUrl: postMedia.url)),
                                  child: displayPostImage(postMedia.url));
                            }),
                        if (widget.hub.medias.length > 1)
                          Padding(
                            padding: const EdgeInsets.only(bottom: 30.0),
                            child: Align(
                              alignment: Alignment.bottomCenter,
                              child: SmoothPageIndicator(
                                controller: _controller,
                                count: widget.hub.medias.length,
                                effect: ScrollingDotsEffect(
                                    dotWidth: 6,
                                    dotHeight: 6,
                                    dotColor: isDarkMode(context)
                                        ? Colors.white54
                                        : Colors.black54,
                                    activeDotColor: Color(COLOR_PRIMARY)),
                              ),
                            ),
                          ),
                      ],
                    ),
                  )
              ),
            ),

            _service.isLoading?
            SliverFillRemaining(
              child: Center(
                child: CircularProgressIndicator(),
              ),
            ):
            SliverList(
              delegate: SliverChildListDelegate([

                if(widget.hub.userId != MyAppState.currentUser!.userID)
                  _isSubLoading?
                  Center(
                    child: Padding(
                      padding: EdgeInsets.all(5),
                      child: CircularProgressIndicator(),
                    ),
                  ):
                  Padding(
                    padding: EdgeInsets.symmetric(horizontal: 5),
                    child: widget.hub.shareUserIds.contains(MyAppState.currentUser!.userID)?
                    ElevatedButton(
                      onPressed: () async {
                        setState(() {
                          _isSubLoading = true;
                        });

                        await Provider.of<HubService>(context, listen: false).unsubscribe(widget.hub);

                        setState(() {
                          _isSubLoading = false;
                          widget.hub.shareUserIds.remove(MyAppState.currentUser!.userID);
                        });
                      },
                      style: ElevatedButton.styleFrom(
                          primary: Colors.red
                      ),
                      child: Text(
                        "Unsubscribe",
                      ),
                    ):
                    ElevatedButton(
                      onPressed: () async {
                        setState(() {
                          _isSubLoading = true;
                        });

                        await Provider.of<HubService>(context, listen: false).subscribe(widget.hub);

                        setState(() {
                          _isSubLoading = false;
                          widget.hub.shareUserIds.add(MyAppState.currentUser!.userID);
                        });
                      },
                      style: ElevatedButton.styleFrom(
                          primary: Colors.green
                      ),
                      child: Text(
                        "Subscribe",
                      ),
                    ),
                  ),

                Padding(
                  padding: EdgeInsets.all(15,),
                  child: Text(
                    widget.hub.name,
                    style: TextStyle(
                        color: Colors.black,
                        fontSize: 18,
                        fontWeight: FontWeight.bold
                    ),
                  ),
                ),

                ..._service.hubResults.map((e) {
                  if(e.isAuction && (e.auctionEnded || DateTime.now().isAfter(e.auctionEndTime??DateTime.now()))) {
                    return Container();
                  }
                  return PostWidget(post: e);
                }).toList(),

                if(_service.noResult)
                  Padding(
                    padding: EdgeInsets.all(20),
                    child: Text(
                      'No results for this hub',
                      style: TextStyle(
                          fontSize: 18,
                          fontWeight: FontWeight.bold
                      ),
                    ),
                  ),

                if(_service.isMore)
                  Center(
                    child: Container(
                      padding: EdgeInsets.all(5),
                      child: CircularProgressIndicator(),
                    ),
                  )

              ]),
            )
          ],
        ),
      )
    );
  }
}

당신은 2개의 람다와 S3를 사용해 볼 수 있습니다.이러한 리소스는 매우 저렴하며 앱이 극단적으로 사용될 때만 요금이 부과됩니다(비즈니스 모델이 좋으면 높은 사용률 -> 더 높은 수입).

첫 번째 람다는 텍스트-문서 매핑을 S3 json 파일로 푸시하는 데 사용됩니다.

두 번째 람다는 기본적으로 검색 api가 될 것이며, s3에서 JSON을 쿼리하고 결과를 반환하는 데 사용할 수 있습니다.

단점은 아마도 s3에서 람다까지의 지연 시간일 것입니다.

Vuejs와 함께 사용합니다.

query(collection(db,'collection'),where("name",">=",'searchTerm'),where("name","<=","~"))

또한 제안과 Firebase 도구를 사용하여 Firebase에 대한 검색 기능을 만들 수 없었기 때문에 .contains() Kotlin 함수를 사용하여 "field-string contains search-string(하위 문자열) 검사"를 직접 만들었습니다.

firestoreDB.collection("products")
        .get().addOnCompleteListener { task->
        if (task.isSuccessful){
            val document = task.result
            if (!document.isEmpty) {
                if (document != null) {
                    for (documents in document) {
                        var name = documents.getString("name")
                        var type = documents.getString("type")
                        if (name != null && type != null) {
                            if (name.contains(text, ignoreCase = true) || type.contains(text, ignoreCase = true)) {
                                // do whatever you want with the document
                            } else {
                                showNoProductsMsg()
                            }
                        }
                    }
                }
                binding.progressBarSearch.visibility = View.INVISIBLE
            } else {
                showNoProductsMsg()
            }
        } else{
            showNoProductsMsg()
        }
    }

먼저 원하는 컬렉션의 모든 문서를 가져온 다음 다음을 사용하여 필터링합니다.

for (documents in document) {
                    var name = documents.getString("name")
                    var type = documents.getString("type")
                    if (name != null && type != null) {
                        if (name.contains(text, ignoreCase = true) || type.contains(text, ignoreCase = true)) {
                            //do whatever you want with this document
                        } else {
                            showNoProductsMsg()
                        }
                    }
                }

저의 경우, 저는 그것들을 모두 필터링했습니다.name제품 및 제품의type그런 다음 부울을 사용했습니다.name.contains(string, ignoreCase = true) OR type.contains(string, ignoreCase = true,string내 앱의 검색창에서 받은 텍스트이고 당신이 사용하기를 추천합니다.ignoreCase = true이 설정이 참이면 문서로 원하는 모든 작업을 수행할 수 있습니다.

Firestore는 숫자만 지원하고 문자열 쿼리를 정확하게 처리하기 때문에 코드가 작동하지 않는 경우에는 이 방법이 가장 좋은 해결 방법이라고 생각합니다.

collection.whereGreaterThanOrEqualTo("name", querySearch)
collection.whereLessThanOrEqualTo("name", querySearch)

천만에요 :) 제가 한 일이 효과가 있으니까요!

import FirebaseFirestoreSwift
@FirestoreQuery(collectionPath: "groceries") var groceries: [Grocery]
groceryResults = groceries.filter({$0.name.lowercased().contains(searchName.lowercased())})

저의 경우, 저는 SO의 태그와 마찬가지로 제한된 키워드 모음으로 각 문서에 태그를 붙입니다.그런 다음 하나 이상의 태그로 문서를 검색할 수 있습니다.

태그 목록이 상대적으로 작은 경우(10K 이하인 경우)에는 클라이언트 측 태그에서 퍼지 검색을 수행할 수 있습니다.

데모 여기에 이미지 설명 입력

분명히 이것이 모든 시나리오에 효과가 있는 것은 아니지만, 저의 경우 탄력적인 검색/알고리즘/등에 대한 "충분히 좋은" 대안이었습니다.

Firebase는 Algolia 또는 Elastic Search for Full-Text 검색을 제안하지만 더 저렴한 대안은 MongoDB일 수 있습니다.가장 저렴한 클러스터(약 US$10/mth)를 사용하여 전체 텍스트를 인덱싱할 수 있습니다.

우리는 백 틱을 사용하여 문자열의 값을 출력할 수 있습니다.이렇게 하면 됩니다.

where('name', '==', `${searchTerm}`)

언급URL : https://stackoverflow.com/questions/46568142/google-firestore-query-on-substring-of-a-property-value-text-search

반응형