205

I am looking to add a simple search field, would like to use something like

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

I tried using where('name', '==', '%searchTerm%'), but it didn't return anything.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
tehfailsafe
  • 3,283
  • 3
  • 21
  • 27
  • 4
    Firebase now supports this. Please update answer: https://stackoverflow.com/a/52715590/2057171 – Albert Renshaw Oct 09 '18 at 07:34
  • 1
    I think the best way is to create a script that manually index each document . Then query those indexes . check this out : https://angularfirebase.com/lessons/typeahead-autocomplete-with-firestore/ – Kiblawi_Rabee May 06 '19 at 07:10
  • Perhaps this [resource](https://medium.com/firebase-tips-tricks/how-to-filter-firestore-data-cheaper-705f5efec444) will help. Here is the corresponding [repo](https://github.com/alexmamo/FilterFirestoreResults). – Alex Mamo Aug 17 '23 at 07:23

28 Answers28

148

I agree with @Kuba's answer, But still, it needs to add a small change to work perfectly for search by prefix. here what worked for me

For searching records starting with name queryText

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

The character \uf8ff used in the query is a very high code point in the Unicode range (it is a Private Usage Area [PUA] code). Because it is after most regular characters in Unicode, the query matches all values that start with queryText.

GabLeRoux
  • 16,715
  • 16
  • 63
  • 81
Ankit Prajapati
  • 2,670
  • 2
  • 12
  • 22
  • 6
    Good answer!, this works great for search of prefix text. To search words in the text, may try "array-contains" implementation as explained in this post https://medium.com/@ken11zer01/firebase-firestore-text-search-and-pagination-91a0df8131ef – guillefd Nov 26 '19 at 13:47
  • Just thinking, but theoretically you could match all values that end with **queryTest** by creating another field and inverting the data... – Jonathan Apr 12 '20 at 23:01
  • Yes @Jonathan, that is also possible. – Ankit Prajapati Apr 13 '20 at 05:15
  • 8
    Just a heads up it's case sensitive. Got stuck on that for a bit, couldn't figure out why nothing was being returned. – odiggity Feb 07 '21 at 06:28
  • if im using this with a `snapshotlistener` how do i get it to auto update in real time after the person serches i dont want them to have to press a button separately – Di Nerd Apps May 01 '21 at 20:03
  • Is it possible to use pagination for this query? – blob Sep 08 '21 at 10:27
  • Worsk perfectly for me!! – rolandforbes Nov 17 '21 at 16:12
75

Full-Text Search, Relevant Search, and Trigram Search!

UPDATE - 2/17/21 - I created several new Full Text Search Options.

See Code.Build for details.


Also, side note, dgraph now has websockets for realtime... wow, never saw that coming, what a treat! Cloud Dgraph - Amazing!


--Original Post--

A few notes here:

1.) \uf8ff works the same way as ~

2.) You can use a where clause or start end clauses:

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

is exactly the same as

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

3.) No, it does not work if you reverse startAt() and endAt() in every combination, however, you can achieve the same result by creating a second search field that is reversed, and combining the results.

Example: First you have to save a reversed version of the field when the field is created. Something like this:

// 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);
}

With this, you can search the last letters of a string field and the first, just not random middle letters or groups of letters. This is closer to the desired result. However, this won't really help us when we want random middle letters or words. Also, remember to save everything lowercase, or a lowercase copy for searching, so case won't be an issue.

4.) If you have only a few words, Ken Tan's Method will do everything you want, or at least after you modify it slightly. However, with only a paragraph of text, you will exponentially create more than 1MB of data, which is bigger than firestore's document size limit (I know, I tested it).

5.) If you could combine array-contains (or some form of arrays) with the \uf8ff trick, you might could have a viable search that does not reach the limits. I tried every combination, even with maps, and a no go. Anyone figures this out, post it here.

6.) If you must get away from ALGOLIA and ELASTIC SEARCH, and I don't blame you at all, you could always use mySQL, postSQL, or neo4Js on Google Cloud. They are all 3 easy to set up, and they have free tiers. You would have one cloud function to save the data onCreate() and another onCall() function to search the data. Simple...ish. Why not just switch to mySQL then? The real-time data of course! When someone writes DGraph with websocks for real-time data, count me in!

Algolia and ElasticSearch were built to be search-only dbs, so there is nothing as quick... but you pay for it. Google, why do you lead us away from Google, and don't you follow MongoDB noSQL and allow searches?

Jonathan
  • 3,893
  • 5
  • 46
  • 77
  • Hello, thanks for the great work. I tried to follow the Solution you created but I could not make a success out of it. When I tried to deploy the function, I get error 'async *getPartitions(desiredPartitionCount)'. what could be responsible for that? – ilatyphi95 Nov 14 '20 at 16:19
  • @ilatyphi95 This is not an error in my code: https://stackoverflow.com/questions/64575650/firebase-functions-error-after-update-what-can-i-do – Jonathan Nov 14 '20 at 16:58
  • Please post any issues: https://github.com/jdgamble555/adv-firestore-functions/issues – Jonathan Nov 14 '20 at 16:59
  • @Jonathan, thanks for the advice, I tried the solution offered in the link you posted but I didn't make any progress either. I have opened an issue on the project github page – ilatyphi95 Nov 15 '20 at 18:45
  • @Jonathan Exciting post! If I followed your article, the algs create lots of little docs. As you know, FS bills by doc reads/writes. Does adding a discussion to your article on the number of docs and their costs make sense? – buttonsrtoys Feb 23 '21 at 14:31
  • 1
    @buttonsrtoys - Relevant Search and Trigram Search only create the same amount of docs as your collection already has. You incur the write only once unless you update the doc, and you incur the read only when you search. This is how reading any document in firestore words. The index in and of itself does not cost much. It is really just the upfront cost of creating one document per document you want to index. – Jonathan Feb 24 '21 at 04:40
  • The ~ character doesn't work the same way as '\uf8ff' ! It's just behind the latin characters. So be careful with this. – pagep Jan 02 '23 at 13:48
63

There's no such operator, allowed ones are ==, <, <=, >, >=.

You can filter by prefixes only, for example for everything that starts between bar and foo you can use

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

You can use external service like Algolia or ElasticSearch for that.

GabLeRoux
  • 16,715
  • 16
  • 63
  • 81
Kuba
  • 1,113
  • 10
  • 26
  • 16
    That's not quite what I'm looking for. I have a large list of products with long titles. "Rebok Mens Tennis Racket". A user might search for `tennis`, but based on the query operators available there's no way to get those results. Combining `>=` and `<=` does not work. Of course I can use Algolia, but I could also just use it with Firebase to do most queries and not need to switch to Firestore... – tehfailsafe Oct 04 '17 at 16:14
  • 8
    @tehfailsafe Well your question is 'how to query if a field contains a string', and the response is 'you can't do that'. – Kuba Oct 04 '17 at 16:24
  • Ah, your "filter by prefixes" was a red herring then. They way you stated it implied that it might work for my use case, which made me think I didn't explain the scenario well enough. – tehfailsafe Oct 04 '17 at 16:31
  • @tehfailsafe I changed the wording a bit (what I meant was that you can *only* filter by prefixes), hope it's clearer now! – Kuba Oct 04 '17 at 16:37
  • 22
    @A.Chakroun what exactly is rude in my answer? – Kuba Jul 12 '18 at 19:51
  • 58
    tbh this is something really necessary. I can't understand why Firebase's team didn't think on this – Dani Sep 22 '18 at 14:59
  • 1
    @Kuba is it case sensite? – John Balvin Arias Feb 13 '19 at 15:58
  • I don't understand why we have to use Algolia for this. How can they have access to this ability and we don't? – Jus10 May 12 '19 at 16:49
  • @Jus10 Algolia doesn't do this "in Firestore", they don't have this ability. You have to copy your data to Algolia and execute the query there. – Kuba May 14 '19 at 14:38
  • Hi all, we still no way to search by suffix, right? – Mi.HTR Dec 25 '19 at 10:43
  • 12
    Really surprise that Firebase is so weak in querying. Can't believe there are so many people are using it if it can't support such a simple query. – Bagusflyer Jun 02 '20 at 06:34
  • @Bagusflyer the beauty of Firestore is that the restrictiveness makes it impossible to write inefficient queries – Charles Fries Jul 09 '21 at 21:55
  • it's not "weak in querying". it has very powerful queries that work very well for firebase's stated use cases. if you want to query your data differently, you should not have used firebase. – Patrick Michaelsen Jul 17 '22 at 22:09
48

While Kuba's answer is true as far as restrictions go, you can partially emulate this with a set-like structure:

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

Now you can query with

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

This works because Firestore will automatically create an index for every field. Unfortunately this doesn't work directly for compound queries because Firestore doesn't automatically create composite indexes.

You can still work around this by storing combinations of words but this gets ugly fast.

You're still probably better off with an outboard full text search.

GabLeRoux
  • 16,715
  • 16
  • 63
  • 81
Gil Gilbert
  • 7,722
  • 3
  • 24
  • 25
  • Is it possible to use cloud function with https://cloud.google.com/appengine/docs/standard/java/search/? – Henry Oct 15 '17 at 09:26
  • 2
    If you're asking this as a follow-up to this answer then: AppEngine's full text search is completely separate from Firestore and so this won't directly help you. You could replicate your data using a cloud function, but that's essentially what the suggestion to use outboard full text search is. If you're asking something else, please start a new question. – Gil Gilbert Oct 16 '17 at 02:16
  • Be aware that you'll need to sanitize keys if you do this. Entries from user input aren't likely to be legal Firestore keys. – James Moore Oct 21 '17 at 20:58
  • 1
    In Firestore, You need to index all terms before using `where` – Husam Jan 02 '18 at 21:21
  • 4
    As Husam mentioned, all of these fields would need to be indexed. I wanted to enable searching for any term my product name contains. So I created an 'object' type property on my document with keys being parts of the product name, each with 'true' value assigned to it, hoping that searching where('nameSegments.tennis', '==', true) would work, but firestore suggests createing an index for nameSegments.tennis, same for every other term. Since there can be infinite number of terms, this answer is usable only for a very limited scennario when all the search terms are defined up front. – Slawoj Jan 14 '18 at 11:34
  • @Slawoj, can you explain what you mean when you say "but firestore suggests createing an index for nameSegments.tennis" ? does firestore suggest? or require? does this where('nameSegments.tennis', '==', true) query work or not? – epeleg Apr 04 '18 at 08:28
  • 2
    @epeleg The query would work, after you created an index for it, yet it's not feasible to create index for each possible term your product name contains, so for text search of terms in product names this approach didn't work for my case. – Slawoj Apr 05 '18 at 09:42
  • 1
    Note: Firebase now supports `array-contains` queries: https://stackoverflow.com/a/52715590/2057171 – Albert Renshaw Oct 09 '18 at 07:34
  • 1
    Array-contains is superior to this approach in every way: you don't have to sanitize input and you can combine array-contains with other criteria in a composite index. – Gil Gilbert Jan 31 '19 at 19:24
46

While Firebase does not explicitly support searching for a term within a string,

Firebase does (now) support the following which will solve for your case and many others:

As of August 2018 they support array-contains query. See: https://firebase.googleblog.com/2018/08/better-arrays-in-cloud-firestore.html

You can now set all of your key terms into an array as a field then query for all documents that have an array that contains 'X'. You can use logical AND to make further comparisons for additional queries. (This is because firebase does not currently natively support compound queries for multiple array-contains queries so 'AND' sorting queries will have to be done on client end)

Using arrays in this style will allow them to be optimized for concurrent writes which is nice! Haven't tested that it supports batch requests (docs don't say) but I'd wager it does since its an official solution.


Usage:

collection("collectionPath").
    where("searchTermsArray", "array-contains", "term").get()
GabLeRoux
  • 16,715
  • 16
  • 63
  • 81
Albert Renshaw
  • 17,282
  • 18
  • 107
  • 195
  • 25
    This is a nice solution. However, correct me if I am wrong, but I think it doesn't allow you to do what @tehfailsafe has asked for. E.g. if you want to get all names that contain the string "abc", you won't get any success with array-contains, as it will only return the documents that have the exact name of "abc", but "abcD" or "0abc" would be out. – Yulian Oct 24 '18 at 15:49
  • 1
    @Yulian In the programming world, `Search term` is typically understood to mean a whole term separated by space, punctuation, etc on both sides. If you google `abcde` right now you will only find results for things like `%20abcde.` or `,abcde!` but not `abcdefghijk..`. even though surely the whole alphabet typed is much more common to be found on the internet, the search is not for abcde* it's for an isolated abcde – Albert Renshaw Oct 24 '18 at 18:29
  • 2
    I see your point and I agree with it, but I was probably mislead by the word `'contains'`, which means exactly what I am referring to in many programming languages. The same goes for `'%searchTerm%'` from an SQL standpoint. – Yulian Oct 25 '18 at 08:48
  • 2
    @Yulian Yeah I get that. Firebase is NoSQL though so it's really good at making these types of operations fast and efficient, even if they may be limited for some out-of-scope problems like wild-card string searching. – Albert Renshaw Oct 25 '18 at 09:05
  • 2
    Well, you could create a separate field for each one with the representation of the words splitted like titleArray: ['this', 'is', 'a', 'title'] every time you update the document. And then the search will be based on that field instead of title. You cold create a triiger onUpdate to create this fields. A lot of work for search based text but I rather have the performance improvements of NoSQL. – sebastianf182 Nov 01 '18 at 15:46
  • 1
    Can you use `array-contains` on strings? since strings are kind of arrays anyways, no? Would be cool if that was the case - otherwise this doesn't answer the question. – Jus10 Dec 05 '18 at 01:36
  • @Jus10 Split string by spaces and add components to array then use array-contains. If you are talking about true wildcard search that has already been addressed in this comment thread. Firebase is built for fast queries (hence the name) if you want slow but in-depth queries don't use firebase, use AWS. – Albert Renshaw Dec 05 '18 at 05:39
  • 2
    This should not be the accepted answer. It's helpful as a suggested workaround but it's not the correct answer to the question posed. @bholben's answer is best, aka "No, Firestore doesn't support that." – briznad Jan 24 '19 at 01:56
  • Worth clarifying that it is not possible to do a logical AND with multiple array_contains queries since "you can include at most one array_contains clause in a compound query" reference: [Compound queries](https://firebase.google.com/docs/firestore/query-data/queries#compound_queries) – Safa Alai Feb 25 '19 at 22:56
  • @SafaAlai Correct, this will have to be multiple queries and the sorting will have to be done on client end. I'll bold the 'for additional queries' and add your link to answer so it's more noticed, thanks! – Albert Renshaw Feb 25 '19 at 23:09
  • @SafaAlai - firebase just added support for a new query called Array-Contains-Any which might address what you were talking about – Albert Renshaw Nov 26 '19 at 18:12
  • But let's say if I want to search a user whose name contains 'abc', what should I do? – Bagusflyer Jun 02 '20 at 06:36
  • @Bagusflyer you can generate every permutation of the name, and put them in an array of search terms. This basically allows you to do contains queries on the name. It's pretty inefficient, but ive used this in production and it was running great until my collection hit about 1k documents. Realistically when searching for a name it's usually passable to do a starts with first or last name instead of full on contains. – rosghub Jun 22 '21 at 02:31
20

Per the Firestore docs, Cloud Firestore doesn't support native indexing or search for text fields in documents. Additionally, downloading an entire collection to search for fields client-side isn't practical.

Third-party search solutions like Algolia and Elastic Search are recommended.

bholben
  • 1,077
  • 1
  • 15
  • 24
  • 79
    I have read the docs, though it's not ideal. The drawback is that Algolia and Firestore have different pricing models... I can happily have 600,000 documents in Firestore (as long as I don't query too many per day). When I push them to Algolia in order to search I now have to pay Algolia $310 per month just to be able to do a title search on my Firestore documents. – tehfailsafe Feb 08 '18 at 19:11
  • 4
    the problem is that this is not free – Dani Sep 22 '18 at 16:19
  • This is the correct answer to the question posed and should be accepted as the best. – briznad Jan 24 '19 at 01:53
20

As of today (18-Aug-2020), there are basically 3 different workarounds, which were suggested by the experts, as answers to the question.

I have tried them all. I thought it might be useful to document my experience with each one of them.

Method-A: Using: (dbField ">=" searchString) & (dbField "<=" searchString + "\uf8ff")

Suggested by @Kuba & @Ankit Prajapati

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

A.1 Firestore queries can only perform range filters (>, <, >=, <=) on a single field. Queries with range filters on multiple fields are not supported. By using this method, you can't have a range operator in any other field on the db, e.g. a date field.

A.2. This method does NOT work for searching in multiple fields at the same time. For example, you can't check if a search string is in any of the fileds (name, notes & address).

Method-B: Using a MAP of search strings with "true" for each entry in the map, & using the "==" operator in the queries

Suggested by @Gil Gilbert

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

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

B.1 Obviously, this method requires extra processing every time data is saved to the db, and more importantly, requires extra space to store the map of search strings.

B.2 If a Firestore query has a single condition like the one above, no index needs to be created beforehand. This solution would work just fine in this case.

B.3 However, if the query has another condition, e.g. (status === "active",) it seems that an index is required for each "search string" the user enters. In other words, if a user searches for "Jam" and another user searches for "Butter", an index should be created beforehand for the string "Jam", and another one for "Butter", etc. Unless you can predict all possible users' search strings, this does NOT work - in case of the query has other conditions!

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

**Method-C: Using an ARRAY of search strings, & the "array-contains" operator

Suggested by @Albert Renshaw & demonstrated by @Nick Carducci

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

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

C.1 Similar to Method-B, this method requires extra processing every time data is saved to the db, and more importantly, requires extra space to store the array of search strings.

C.2 Firestore queries can include at most one "array-contains" or "array-contains-any" clause in a compound query.

General Limitations:

  1. None of these solutions seems to support searching for partial strings. For example, if a db field contains "1 Peter St, Green District", you can't search for the string "strict."
  2. It is almost impossible to cover all possible combinations of expected search strings. For example, if a db field contains "1 Mohamed St, Green District", you may NOT be able to search for the string "Green Mohamed", which is a string having the words in a different order than the order used in the DB field.

There is no one solution that fits all. Each workaround has its limitations. I hope the information above can help you during the selection process between these workarounds.

For a list of Firestore query conditions, please check out the documentation https://firebase.google.com/docs/firestore/query-data/queries.

I have not tried https://fireblog.io/blog/post/firestore-full-text-search, which is suggested by @Jonathan.

Bilal Abdeen
  • 1,627
  • 1
  • 19
  • 41
19

I'm sure Firebase will come out with "string-contains" soon to capture any index[i] startAt in the string... But I’ve researched the webs and found this solution thought of by someone else set up your data like this

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
  });

enter image description here

query like this

firebase.firestore()
    .collection("clubs")
    .where(
        "titleAsArray",
        "array-contains",
        this.state.userQuery.toLowerCase()
    )
GabLeRoux
  • 16,715
  • 16
  • 63
  • 81
  • 5
    Not recommended at all. As documents have 20k lines limit, so you just cannot utilise the it this way, until u r sure that ur document will never reach such limits – Sandeep May 20 '20 at 20:13
  • 1
    It's the best option at the moment, what else is recommended? – Nick Carducci for Carface Bank May 21 '20 at 17:49
  • 1
    @Sandeep I'm pretty sure that size is limited to 1MB of size and 20 levels of depth per document. What do you mean by 20k lines? This is currently the best workaround if using Algolia or ElasticSearch is out of the table – perepm Jul 22 '20 at 10:02
  • 1
    @ppicom The document has a limit of 20k lines in side it. It means you cannot have an array above 19999k size where as 1 line is for array name. This also means u cannot add any other fields in a document as the limit is reached – Sandeep Nov 21 '20 at 07:09
  • 1
    I'm quite sure @NickCarducci is right. The limit is regarding depth, not span. Nevertheless, this solution works with fields that will contain one or two words, three tops (I've found it particularly useful when searching users stored in firestore by username, email and such, all at the same time). Otherwise you might reach de 1MB limit pretty quick. – perepm Nov 21 '20 at 14:01
  • Doesn't work for "nitt" – Adrian Bartholomew Feb 13 '23 at 12:10
14

Late answer but for anyone who's still looking for an answer, Let's say we have a collection of users and in each document of the collection we have a "username" field, so if want to find a document where the username starts with "al" we can do something like

FirebaseFirestore.getInstance()
    .collection("users")
    .whereGreaterThanOrEqualTo("username", "al")
GabLeRoux
  • 16,715
  • 16
  • 63
  • 81
MoTahir
  • 863
  • 7
  • 22
  • this is a great an easy solution thank you. But what if you want to check more than one field. Like "name" and "description" connected by an OR? – Try it Jun 15 '19 at 14:31
  • I don't think you can query based on two fields, sadly firebase is bad when it comes to querying, you can check this hope it helps https://stackoverflow.com/questions/26700924/query-based-on-multiple-where-clauses-in-firebase – MoTahir Jun 15 '19 at 21:29
  • 1
    Confirmed, @MoTahir. There is no "OR" in Firestore. – Rap Jul 16 '19 at 21:06
  • that solution does not match usernames starting with "al"... for example "hello" would be matched ("hello" > "al") – Antoine Feb 29 '20 at 19:35
  • 1
    Querying by OR is simply a matter of combining two search results. Sorting those results is a different problem... – Jonathan Apr 12 '20 at 22:57
  • only does prefix search. :( what if youre searching on the 2nd word? "meet bob" and you start searching for "b". – not_fubar_yet Sep 02 '23 at 17:21
9

I just had this problem and came up with a pretty simple solution.

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

The isGreaterThanOrEqualTo lets us filter out the beginning of our search and by adding a "z" to the end of the isLessThanOrEqualTo we cap our search to not roll over to the next documents.

Jacob Bonk
  • 303
  • 3
  • 8
  • 3
    Ive tried this solution but for me it only works when the full string is entered. For example, if I want to get the term "free", if i start typing in "fr" nothing will return. Once I type in "free" then the term gives me its snapshot. – Chris May 23 '19 at 20:16
  • Are you using the same code format? And is the term a string in firestore? I know that you cant filter by the documentId. – Jacob Bonk May 23 '19 at 20:26
8

EDIT 05/2021:

Google Firebase now has an extension to implement Search with Algolia. Algolia is a full text search platform that has an extensive list of features. You are required to have a "Blaze" plan on Firebase and there are fees associated with Algolia queries, but this would be my recommended approach for production applications. If you prefer a free basic search, see my original answer below.

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

ORIGINAL ANSWER:

The selected answer only works for exact searches and is not natural user search behavior (searching for "apple" in "Joe ate an apple today" would not work).

I think Dan Fein's answer above should be ranked higher. If the String data you're searching through is short, you can save all substrings of the string in an array in your Document and then search through the array with Firebase's array_contains query. Firebase Documents are limited to 1 MiB (1,048,576 bytes) (Firebase Quotas and Limits) , which is about 1 million characters saved in a document (I think 1 character ~= 1 byte). Storing the substrings is fine as long as your document isn't close to 1 million mark.

Example to search user names:

Step 1: Add the following String extension to your project. This lets you easily break up a string into substrings. (I found this here).

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])
}

Step 2: When you store a user's name, also store the result of this function as an array in the same Document. This creates all variations of the original text and stores them in an array. For example, the text input "Apple" would creates the following array: ["a", "p", "p", "l", "e", "ap", "pp", "pl", "le", "app", "ppl", "ple", "appl", "pple", "apple"], which should encompass all search criteria a user might enter. You can leave maximumStringSize as nil if you want all results, however, if there is long text, I would recommend capping it before the document size gets too big - somewhere around 15 works fine for me (most people don't search long phrases anyway).

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
}

Step 3: You can use Firebase's array_contains function!

[yourDatabasePath].whereField([savedSubstringArray], arrayContains: searchText).getDocuments....
GabLeRoux
  • 16,715
  • 16
  • 63
  • 81
nicksarno
  • 3,850
  • 1
  • 13
  • 33
  • Following my solution, you would have to split each and every word, in addition to the sentence as a whole. That's why I mention in my solution that this approach is not valid for fields of text that will contain more than one or two words. The objective is to get every possible combination in the array. – perepm Nov 21 '20 at 13:58
8

I used trigram just like Jonathan said it.

trigrams are groups of 3 letters stored in a database to help with searching. so if I have data of users and I let' say I want to query 'trum' for donald trump I have to store it this way

enter image description here

and I just to recall this way

 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']);
        }
      });

that will get correct result no matter what

enter image description here

6

I actually think the best solution to do this within Firestore is to put all substrings in an array, and just do an array_contains query. This allows you to do substring matching. A bit overkill to store all substrings but if your search terms are short it's very very reasonable.

Thingamajig
  • 4,107
  • 7
  • 33
  • 61
5

If you don't want to use a third-party service like Algolia, Firebase Cloud Functions are a great alternative. You can create a function that can receive an input parameter, process through the records server-side and then return the ones that match your criteria.

Rap
  • 6,851
  • 3
  • 50
  • 88
  • 1
    What about android? – Pratik Butani Sep 17 '19 at 12:47
  • Are you proposing that people iterate over every single record in a collection? – DarkNeuron Apr 16 '20 at 15:37
  • Not really. I'd use Array.prototype.* - Like .every(), .some(), .map(), .filter(), etc. This is done in Node on the server in a Firebase Function before returning values to the client. – Rap Apr 16 '20 at 17:22
  • 11
    You would still have to read ALL the documents to search them, which incurs expenses and is expensive for Time. – Jonathan Apr 30 '20 at 05:44
  • Sorry to say, but this solution is not appropiate. If you have that many records, that you cannot filter them client side, you should also not filter them like this in a cloud function. – Volker Andres Mar 12 '22 at 12:21
4

As of March 2023, Firestore's new OR queries allow to remove the issue that prefix searches are case-sensitive (to some extend):

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')
    )
  )
);
ErikWittern
  • 1,083
  • 9
  • 13
1

This worked for me perfectly but might cause performance issues.

Do this when querying firestore:

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

Do this in your FutureBuilder:

    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);
            })
   };
1

Following code snippet takes input from user and acquires data starting with the typed one.

Sample Data:

Under Firebase Collection 'Users'

user1: {name: 'Ali', age: 28},

user2: {name: 'Khan', age: 30},

user3: {name: 'Hassan', age: 26},

user4: {name: 'Adil', age: 32}

TextInput: A

Result:

{name: 'Ali', age: 28},

{name: 'Adil', age: 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);

};
Shahjahan
  • 87
  • 4
1

Same as @nicksarno but with a more polished code that doesn't need any extension:

Step 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
}

Step 2

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

Step 3

Firestore.firestore().collection("Users")
  .whereField("nameSubstrings", arrayContains: searchText)
  .getDocuments...
Lorenzo Fiamingo
  • 3,251
  • 2
  • 17
  • 35
1

2021 Update

Took a few things from other answers. This one includes:

  • Multi word search using split (acts as OR)
  • Multi key search using flat

A bit limited on case-sensitivity, you can solve this by storing duplicate properties in uppercase. Ex: 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);
}

Note: the number of Firebase calls is equivalent to the number of words in the query string * the number of keys you're searching on.

BuffMcBigHuge
  • 579
  • 5
  • 8
0

With Firestore you can implement a full text search but it will still cost more reads than it would have otherwise, and also you'll need to enter and index the data in a particular way, So in this approach you can use firebase cloud functions to tokenise and then hash your input text while choosing a linear hash function h(x) that satisfies the following - if x < y < z then h(x) < h (y) < h(z). For tokenisation you can choose some lightweight NLP Libraries in order to keep the cold start time of your function low that can strip unnecessary words from your sentence. Then you can run a query with less than and greater than operator in Firestore. While storing your data also, you'll have to make sure that you hash the text before storing it, and store the plain text also as if you change the plain text the hashed value will also change.

0

Typesense service provide substring search for Firebase Cloud Firestore database.

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

Following is the relevant codes of typesense integration for my project.

lib/utils/typesense.dart

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(),
                    ),
                  )

              ]),
            )
          ],
        ),
      )
    );
  }
}
benjixinator
  • 129
  • 1
  • 12
persec10000
  • 820
  • 2
  • 10
  • 17
0

You can try using 2 lambdas and S3. These resources are very cheap and you will only be charged once the app has extreme usage ( if the business model is good then high usage -> higher income).

The first lambda will be used to push a text-document mapping to an S3 json file.

the second lambda will basically be your search api, you will use it to query the JSON in s3 and return the results.

The drawback will probably be the latency from s3 to lambda.

0

I use this with Vue js

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

I also couldn't manage to create a search function to Firebase using the suggestions and Firebase tools so I created my own "field-string contains search-string(substring) check", using the .contains() Kotlin function:

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()
        }
    }

First, you get ALL the documents in the collection you want, then you filter them using:

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()
                        }
                    }
                }

In my case, I filtered them all by the name of the product and its type, then I used the boolean name.contains(string, ignoreCase = true) OR type.contains(string, ignoreCase = true, string is the text I got in the search bar of my app and I recommend you to use ignoreCase = true. With this setence being true, you can do whatever you want with the document.

I guess this is the best workaround since Firestore only supports number and exacts strings queries, so if your code didn't work doing this:

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

You're welcome :) because what I did works!

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

In my case, I tag each of my documents with a finite collection of keywords (much like tags on SO). Then I can search documents by one or more tags.

As long as your list of tags is relatively small (10K or less, perhaps), you can do fuzzy searching on the tags, client side.

Demo enter image description here

Obviously this won't work for every scenario, but in my case it was a "good enough" alternative to elasticsearch / algolia / etc.

Ben
  • 20,038
  • 30
  • 112
  • 189
-1

Firebase suggests Algolia or ElasticSearch for Full-Text search, but a cheaper alternative might be MongoDB. The cheapest cluster (approx US$10/mth) allows you to index for full-text.

MFB
  • 19,017
  • 27
  • 72
  • 118
  • I thought this was an answer trying to sell services you own, but for any skeptics like me.. Source: https://firebase.google.com/docs/firestore/solutions/search – C. Skjerdal Mar 12 '21 at 17:25
-14

We can use the back-tick to print out the value of a string. This should work:

where('name', '==', `${searchTerm}`)
Zach J
  • 7
  • 2
  • Thanks, but this question is about getting non-exact values. For example, the example in question does work find if the name is exact. If I have doc with name: "Test" and then I search for "Test", it works. But I would expect to be able to search for "tes" or "est" and still get the "Test" result. Imagine a use case with book titles. People often search for partial book titles rather than the precise title entirely. – tehfailsafe Nov 09 '17 at 17:25
  • 23
    @suulisin you are right, I did not read it carefully as I was to eager to share what I had found. Thank you for your effort to point that out, and I will be more careful – Zach J Mar 30 '18 at 14:18