forked from firebase/functions-samples
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
359 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"hosting": { | ||
"public": "public" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
test.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |