Skip to content

Commit

Permalink
Adds full-text-firestore
Browse files Browse the repository at this point in the history
  • Loading branch information
abeisgoat committed Sep 30, 2017
1 parent d42b34a commit 41c9686
Show file tree
Hide file tree
Showing 7 changed files with 359 additions and 0 deletions.
34 changes: 34 additions & 0 deletions fulltext-search-firestore/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Full Text search via Algolia

This template shows how to enable full text search on Cloud Firestore documents by using an [Algolia](https://algolia.com) hosted search service.

## Functions Code

See file [functions/index.js](functions/index.js) for the code.

The dependencies are listed in [functions/package.json](functions/package.json).

## Sample Database Structure

As an example we'll be using a secure note structure:

```
/notes
/note-123456
text: "This is my first note...",
author: "FIREBASE_USER_ID"
/note-123457
text: "This is my second note entry...",
author: "FIREBASE_USER_ID"
tags: ["some_category"]
```

Whenever a new note is created or modified a Function sends the content to be indexed to the Algolia instance.

To securely search notes, a user is issued a [Secured API Key](https://www.algolia.com/doc/guides/security/api-keys/#secured-api-keys) from Algolia which
limits which documents they can search through.

## Setting up the sample

For setup and overview, please see the [Full Text Search Solution](https://firebase.google.com/docs/firestore/solutions/search) in the
Cloud Firestore documentation.
5 changes: 5 additions & 0 deletions fulltext-search-firestore/firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"hosting": {
"public": "public"
}
}
1 change: 1 addition & 0 deletions fulltext-search-firestore/functions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test.js
127 changes: 127 additions & 0 deletions fulltext-search-firestore/functions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const functions = require("firebase-functions");
const algoliasearch = require("algoliasearch");

// [START init_algolia]
// Initialize Algolia, requires installing Algolia dependencies:
// https://www.algolia.com/doc/api-client/javascript/getting-started/#install
//
// App ID and API Key are stored in functions config variables
const ALGOLIA_ID = functions.config().algolia.app_id;
const ALGOLIA_ADMIN_KEY = functions.config().algolia.api_key;
const ALGOLIA_SEARCH_KEY = functions.config().algolia.search_key;

const ALGOLIA_INDEX_NAME = "notes";
const client = algoliasearch(ALGOLIA_ID, ALGOLIA_ADMIN_KEY);
// [END init_algolia]

// [START update_index_function]
// Update the search index every time a blog post is written.
exports.onNoteCreated = functions.firestore.document("notes/{noteId}").onCreate(event => {
// Get the note document
const note = event.data.data();

// Add an "objectID" field which Algolia requires
note.objectID = event.params.noteId;

// Write to the algolia index
const index = client.initIndex(ALGOLIA_INDEX_NAME);
return index.saveObject(note);
});
// [END update_index_function]

// [START get_firebase_user]
const admin = require("firebase-admin");
admin.initializeApp(functions.config().firebase);

function getFirebaseUser(req, res, next) {
console.log("Check if request is authorized with Firebase ID token");

if (
!req.headers.authorization ||
!req.headers.authorization.startsWith("Bearer ")
) {
console.error(
"No Firebase ID token was passed as a Bearer token in the Authorization header.",
"Make sure you authorize your request by providing the following HTTP header:",
"Authorization: Bearer <Firebase ID Token>"
);
res.status(403).send("Unauthorized");
return;
}

let idToken;
if (
req.headers.authorization &&
req.headers.authorization.startsWith("Bearer ")
) {
console.log("Found 'Authorization' header");
idToken = req.headers.authorization.split("Bearer ")[1];
}

admin
.auth()
.verifyIdToken(idToken)
.then(decodedIdToken => {
console.log("ID Token correctly decoded", decodedIdToken);
req.user = decodedIdToken;
next();
})
.catch(error => {
console.error("Error while verifying Firebase ID token:", error);
res.status(403).send("Unauthorized");
});
}
// [END get_firebase_user]

// [START get_algolia_user_token]
// This complex HTTP function will be created as an ExpressJS app:
// https://expressjs.com/en/4x/api.html
const app = require("express")();

// We'll enable CORS support to allow the function to be invoked
// from our app client-side.
app.use(require("cors")({ origin: true }));

// Then we'll also use a special "getFirebaseUser" middleware which
// verifies the Authorization header and adds a `user` field to the
// incoming request:
// https://gist.github.com/abehaskins/832d6f8665454d0cd99ef08c229afb42
app.use(getFirebaseUser);

// Add a route handler to the app to generate the secured key
app.get("/", (req, res) => {
// Create the params object as described in the Algolia documentation:
// https://www.algolia.com/doc/guides/security/api-keys/#generating-api-keys
const params = {
// This filter ensures that only documents where author == user_id will be readable
filters: `author:${req.user.user_id}`,
// We also proxy the user_id as a unique token for this key.
userToken: req.user.user_id
};

// Call the Algolia API to generate a unique key based on our search key
const key = client.generateSecuredApiKey(ALGOLIA_SEARCH_KEY, params);

// Then return this key as {key: "...key"}
res.json({ key });
});

// Finally, pass our ExpressJS app to Cloud Functions as a function
// called "getSearchKey";
exports.getSearchKey = functions.https.onRequest(app);
// [END get_algolia_user_token]
16 changes: 16 additions & 0 deletions fulltext-search-firestore/functions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "full-text-search",
"description": "Full text search for Firestore",
"dependencies": {
"algoliasearch": "^3.24.0",
"cors": "^2.8.4",
"express": "^4.16.1",
"@google-cloud/firestore": "^0.7.0",
"firebase-admin": "^5.0.0",
"firebase-functions": "^0.7.0"
},
"private": true,
"devDependencies": {
"prettier": "^1.6.1"
}
}
69 changes: 69 additions & 0 deletions fulltext-search-firestore/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<!-- /**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ -->
<html>
<head>
<title>Full Text Search via Algolia and Cloud Firestore</title>
<style>
#content > * {
width: 600px;
clear: both;
}

#results {
min-height: 20px;
border: 1px solid #ccc;
}
</style>
</head>
<body>
<div id="content">
<textarea id="note-text"></textarea>
<button id="do-add-note">Add Note to Firestore / Algolia</button>
<br /><br />
<input type="text" id="query-text" placeholder="Search your notes..." />
<button id="do-query">Search</button>
<br />
<h3>Results</h3>
<pre id="results">

</pre>
<br />
<b>Make sure to follow setup steps in the
<a href="https://firebase.google.com/docs/firestore/solutions/search">documentation</a>
</b><br /><br />
<i>
Open your JavaScript console for helpful errors.
<br /><br />
Indexing can take several seconds and may require a page refresh for new<br />
notes to appear due to Algolia's search client caching results.
</i>
</div>

<!-- Import the Algolia SDK -->
<script src="https://cdn.jsdelivr.net/algoliasearch/3/algoliasearch.min.js"></script>

<!-- Import and configure the Firebase SDK -->
<!-- These scripts are made available when the app is served or deployed on Firebase Hosting -->
<!-- If you do not serve/host your project using Firebase Hosting see https://firebase.google.com/docs/web/setup -->
<script src="/__/firebase/4.5.0/firebase-app.js"></script>
<script src="/__/firebase/4.5.0/firebase-auth.js"></script>
<script src="/__/firebase/0.5,0/firebase-firestore.js"></script>

<script src="/__/firebase/init.js"></script>

<script src="./index.js"></script>
</body>
</html>
107 changes: 107 additions & 0 deletions fulltext-search-firestore/public/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const PROJECT_ID = "" // Required - your Firebase project ID
const ALGOLIA_APP_ID = ""; // Required - your Algolia app ID
const ALGOLIA_SEARCH_KEY = ""; // Optional - Only used for unauthenticated search

function unauthenticated_search(query) {

// [START search_index_unsecure]
const client = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_KEY);
const index = client.initIndex("notes");

// Perform an Algolia search:
// https://www.algolia.com/doc/api-reference/api-methods/search/
index
.seach({
query
})
.then(responses => {
// Response from Algolia:
// https://www.algolia.com/doc/api-reference/api-methods/search/#response-format
console.log(responses.hits);
});
// [END search_index_unsecure]
}

function authenticated_search(query) {
let client, index;
// [START search_index_secure]
// Use Firebase Authentication to request the underlying token
return firebase.auth().currentUser.getIdToken()
.then(function (token) {
// The token is then passed to our getSearchKey Cloud Function
return fetch(`https://us-central1-${PROJECT_ID}.cloudfunctions.net/getSearchKey/`, {
headers: { Authorization: `Bearer ${token}` }
});
})
.then(function (response) {
// The Fetch API returns a stream, which we convert into a JSON object.
return response.json();
})
.then(function (data) {
// Data will contain the restricted key in the `key` field.
client = algoliasearch(ALGOLIA_APP_ID, data.key);
index = client.initIndex("notes");

// Perform the search as usual.
return index.search({query});
})
.then(responses => {
// Finally, use the search "hits" returned from Algolia.
return responses.hits;
});
// [END search_index_secure]
}

function search(query) {
if (!PROJECT_ID) {
console.warn("Please set PROJECT_ID in /index.js!");
} else if (!ALGOLIA_APP_ID) {
console.warn("Please set ALGOLIA_APP_ID in /index.js!");
} else if (ALGOLIA_SEARCH_KEY) {
console.log("Performing unauthenticated search...");
return unauthenticated_search(query);
} else {
return firebase.auth().signInAnonymously()
.then(function () {
return authenticated_search(query).catch((err) => {
console.warn(err);
});
}).catch(function (err) {
console.warn(err);
console.warn("Please enable Anonymous Authentication in your Firebase Project!");
});
}
}

// Other code to wire up the buttons and textboxes.

document.querySelector("#do-add-note").addEventListener("click", function () {
firebase.firestore().collection("notes").add({
author: [firebase.auth().currentUser.uid],
text: document.querySelector("#note-text").value
}).then(function () {
document.querySelector("#note-text").value = "";
});
});

document.querySelector("#do-query").addEventListener("click", function () {
search(document.querySelector("#query-text").value).then(function (hits) {
document.querySelector("#results").innerHTML = JSON.stringify(hits, null, 2);
});
});

0 comments on commit 41c9686

Please sign in to comment.