In some cases, you don’t want your users to search your whole set of
records. You want them to search only the subset that is specifically tailored to
them, like all the movies they bookmarked, or all the items they added to their
shopping list, or all the content shared by their friends.
Dataset - with an Attribute for Filtering Users
Because Algolia is schemaless and does not have any concept of the relationship
between objects, you’ll need to put all of the relevant information in each record.
Let’s use a blog post dataset as an example, where each post should be visible only to some users.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| [
{
"objectID": "myID1",
"name": "Mobile Search UX",
"author_name": "Bob",
"viewable_by": [1, 2, 3]
},
{
"objectID": "myID2",
"name": "Keeping mobile apps lightweight",
"author_name": "Henry",
"viewable_by": [3]
},
{
"objectID": "myID3",
"name": "Comparing Algolia and Elasticsearch",
"author_name": "Doug",
"viewable_by": [1, 2]
}
]
|
As you can see, each record contains a viewable_by
array containing a list of user IDs: only users listed in this array can view the post.
We have chosen to name this attribute viewable_by
, but you are free to come up with your own name.
You can download the dataset here.
Have look at how to import it in Algolia here.
Initialize Client
1
2
3
4
5
6
7
8
9
10
11
12
| // composer autoload
require __DIR__ . '/vendor/autoload.php';
// if you are not using composer
// require_once 'path/to/algoliasearch.php';
$client = \Algolia\AlgoliaSearch\SearchClient::create(
'YourApplicationID',
'YourAdminAPIKey'
);
$index = $client->initIndex('your_index_name');
|
1
2
3
4
5
6
| require 'rubygems'
require 'algoliasearch'
Algolia.init(application_id: 'YourApplicationID',
api_key: 'YourAdminAPIKey')
index = Algolia::Index.new('your_index_name')
|
1
2
3
4
5
6
7
8
9
10
11
| // const algoliasearch = require('algoliasearch');
// const algoliasearch = require('algoliasearch/reactnative');
// const algoliasearch = require('algoliasearch/lite');
// import algoliasearch from 'algoliasearch';
//
// or just use algoliasearch if you are using a <script> tag
// if you are using AMD module loader, algoliasearch will not be defined in window,
// but in the AMD modules of the page
const client = algoliasearch('YourApplicationID', 'YourAdminAPIKey');
const index = client.initIndex('your_index_name');
|
1
2
3
4
| from algoliasearch.search_client import SearchClient
client = SearchClient.create('YourApplicationID', 'YourAdminAPIKey')
index = client.init_index('your_index_name')
|
1
2
| let client = Client(appID: "YourApplicationID", apiKey: "YourAdminAPIKey")
let index = client.index(withName: "your_index_name")
|
We released a Kotlin API client, which is better suited for Android development. Check it out here.
1
2
| Client client = new Client("YourApplicationID", "YourAdminAPIKey");
Index index = client.getIndex("your_index_name");
|
1
2
| SearchClient client = new SearchClient("YourApplicationID", "YourAdminAPIKey");
SearchIndex index = client.InitIndex("your_index_name");
|
1
2
3
4
| SearchClient client =
DefaultSearchClient.create("YourApplicationID", "YourAdminAPIKey");
SearchIndex index = client.InitIndex("your_index_name");
|
1
2
3
4
5
6
7
8
| package main
import "github.com/algolia/algoliasearch-client-go/algolia/search"
func main() {
client := search.NewClient("YourApplicationID", "YourAdminAPIKey")
index := client.InitIndex("your_index_name")
}
|
1
2
| // No initIndex
val client = new AlgoliaClient("YourApplicationID", "YourAdminAPIKey")
|
1
2
3
4
5
6
7
| val client = ClientSearch(
applicationID = ApplicationID("latency"),
apiKey = APIKey("YourAdminAPIKey")
)
val indexName = IndexName("your_index_name")
client.initIndex(indexName)
|
Making the Attribute Filterable
To make your viewable_by
attribute filterable, you should add it in attributesForFaceting
.
1
2
3
4
5
| $index->setSettings([
'attributesForFaceting' => [
'filterOnly(viewable_by)'
]
]);
|
1
2
3
4
5
| index.set_settings(
attributesForFaceting: [
'filterOnly(viewable_by)'
]
)
|
1
2
3
4
5
6
7
| IndexSettings settings = new IndexSettings
{
AttributesForFaceting = new List<string>
{
"filterOnly(viewable_by)"
}
};
|
1
2
| index.setSettings(new IndexSettings()
.setAttributesForFaceting(Collections.singletonList("viewable_by")));
|
1
2
3
| res, err := index.SetSettings(search.Settings{
AttributesForFaceting: opt.AttributesForFaceting("filterOnly(viewable_by)"),
})
|
1
2
3
4
5
6
7
| val settings = settings {
attributesForFaceting {
+FilterOnly("viewable_by")
}
}
index.setSettings(settings)
|
Because in this example we are only going to filter on this attribute, we added the filterOnly
modifier.
This is better from a performance point of view, because Algolia won’t have to compute the count for each value.
If you need faceting on this attribute, just remove the filterOnly
modifier.
Adding and Removing Users
Whenever someone bookmarks/unbookmarks a blog post, you’ll have to update the
viewable_by
array to add/remove the corresponding user_id
.
1
2
3
4
5
6
| $index->partialUpdateObject(
[
'viewable_by' => [1, 2],
'objectID' => 'myID1'
]
);
|
1
2
3
4
| index.partial_update_object({
viewable_by: [1, 2],
objectID: 'myID1'
})
|
1
2
3
4
5
6
7
8
| class DemoClass {
// getters setters omitted
@JsonProperty("viewable_by")
private List<String> viewableBy;
}
index.partialUpdateObject(new DemoClass()
.setViewableBy(Arrays.aslist("1", "2")));
|
1
2
3
4
| res, err := index.PartialUpdateObject(map[string]interface{}{
"viewable_by": []int{1, 2},
"objectID": "myID1",
})
|
1
2
3
4
5
6
7
8
9
10
| index.partialUpdateObject(
ObjectID("myID1"),
Partial.Update(
Attribute("viewable_bly"),
jsonArray {
+(1 as Number)
+(2 as Number)
}
)
)
|
In the previous snippet we replaced the content of the attribute completely using the
partial update method.
This partial update method also allows you to
update an attribute in different ways, like adding/removing a element to an array.
Generating a Secured API Key (in the backend)
If your search is completely done in the front-end, malicious users could tweak the request
and get access to content they are not allowed to see.
To prevent this, we are going generate a
Secured API Key,
which is a special API key with a set of filters.
Those filters are pre-integrated inside the key and therefore cannot be changed by the user.
1
2
3
4
5
6
7
8
| $currentUserID = 1; // Replace by the current user ID
$securedApiKey = \Algolia\AlgoliaSearch\Client::generateSecuredApiKey(
'SearchOnlyApiKeyKeptPrivate', // Make sure to use a search key
[
'filters' => 'viewable_by:'.$currentUserID
]
);
|
1
2
3
4
5
6
| current_user_id = 1 # Replace by the current user ID
secured_api_key = Algolia.generate_secured_api_key(
'your_search_only_api_key_kept_private', # Make sure to use a search key
{ filters: 'viewable_by:' + current_user_id.to_s }
)
|
1
2
3
4
5
6
7
8
9
10
| // Only works in Node
const currentUserID = 1; // Replace by the current user ID
const publicKey = client.generateSecuredApiKey(
'SearchOnlyApiKeyKeptPrivate', // Make sure to use a search key
{
filters: `viewable_by:${currentUserID}`
}
);
|
1
2
3
4
5
6
7
8
9
| from algoliasearch.search_client import SearchClient
current_user_id = 1 # Replace by the current user ID
public_key = SearchClient.generate_secured_api_key(
'SearchOnlyApiKeyKeptPrivate', { # Make sure to use a search key
'filters': 'viewable_by:' + str(current_user_id)
}
)
|
1
2
3
4
5
6
7
8
9
| int currentUserId = 1; // Replace by the current user ID
SecuredApiKeyRestriction restriction = new SecuredApiKeyRestriction
{
Query = new Query { Filters = $"viewable_by:{currentUserId}" },
};
// Make sure to use a search key
client.GenerateSecuredApiKeys("SearchOnlyApiKeyKeptPrivate", restriction);
|
1
2
3
4
5
6
7
8
9
10
| int currentUserID = 1; // Replace by the current user ID
SecuredApiKeyRestriction restriction =
new SecuredApiKeyRestriction()
.setQuery(new Query().setFilters(String.format("viewable_by:%s", currentUserID)));
String publicKey = client.generateSecuredAPIKey(
"SearchOnlyApiKeyKeptPrivate", // Make sure to use a search key
restriction
);
|
1
2
3
4
5
6
7
| currentUserID := 1 // Replace by the current user ID
filter := fmt.Sprintf("viewable_by:%d", currentUserID)
key, err := search.GenerateSecuredAPIKey(
"YourSearchOnlyApiKeyKeptPrivate", // Make sure to use a search key
opt.Filters(filter),
)
|
1
2
3
4
5
6
| val currentUserID: Int = 1 // Replace by the current user ID
val publicKey = client.generateSecuredApiKey(
"SearchOnlyApiKeyKeptPrivate", // Make sure to use a search key
Query(filters = Some("viewable_by:%s".format(currentUserID)))
)
|
1
2
3
4
5
| val currentUserId = 1 // Replace by the current user ID
val restriction = SecuredAPIKeyRestriction(Query(filters = "viewable_by:$currentUserId"))
// Make sure to use a search key
ClientSearch.generateAPIKey(APIKey("SearchOnlyApiKeyKeptPrivate"), restriction)
|
For every user we’ll use this code to generate a key containing the right filter.
To invalidate the generated Secured API Keys, you’ll need to invalidate the search API key they have been generated from.
Searching in the subset (in the frontend)
1
2
3
4
5
| val securedApiKey = APIKey("Secured API Key for current user") // Use the key generated earlier
val client = ClientSearch(ApplicationID("YourApplicationID"), securedApiKey)
val index = client.initIndex(IndexName("your_index_name"))
index.search(Query("query"))
|
1
2
3
4
5
6
7
8
9
| const securedApiKey = "Secured API Key for current user"; // Use the key generated earlier
const client = algoliasearch("YourApplicationID", securedApiKey);
const index = client.initIndex('your_index_name');
// only query string
index.search('query string', (err, content) => {
console.log(content.hits);
});
|
The user ID filter being inside the key, we do a normal search and it will
return only records which have the current user ID inside its viewable_by
attribute.