Algolia matches queries from the beginning of words (prefix matches),
but it doesn’t match queries that appear in the middle (infix) or at the end (suffix).
For example, searching for “note” matches “notepad” and “notebook”,
but not “keynote”.
This can cause issues when users search for a product reference without its internal prefix,
such as searching “ABCDEF” for an item stored as “XXXABCDEF” in your index.
To support infix and suffix matching,
generate every possible suffix of a string
(for example, “ABCDEF”, “BCDEF”, “CDEF”, “DEF”, “EF”, “F”).
Create suffix attributes
In the following product dataset,
the product_reference
attribute has a numeric prefix, used for internal purposes.
1
2
3
4
5
6
7
8
9
10
| [
{
"name": "Apple iPhone 16",
"product_reference": "002ABCDEF"
},
{
"name": "Apple iPhone 16 Plus",
"product_reference": "001GHIJKL"
}
]
|
The function shown below generates an attribute that contains all possible suffixes for product_reference
, as an array.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| $objects = json_decode(file_get_contents('products.json'), true);
$objects = array_map(function ($record) {
$reference = $record['product_reference'];
$record['product_reference_suffixes'] = [];
while (mb_strlen($reference) > 1) {
$reference = substr($reference, 1);
$record['product_reference_suffixes'][] = $reference;
}
return $record;
}, $objects);
$index->saveObjects($objects, ['autoGenerateObjectIDIfNotExist' => true]);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| require 'json'
file = File.read(products.json)
objects = JSON.parse(file)
records = objects.map do |object|
word = object['product_reference']
word_size = word.size
object['product_reference_suffixes'] = []
1.upto(word_size - 1) do |i|
object['product_reference_suffixes'] << word[i..word_size]
end
object
end
index.save_objects(records, { autoGenerateObjectIDIfNotExist: true })
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| let objects = require("./products.json");
objects = objects.map(record => {
let reference = record.product_reference;
record.product_reference_suffixes = [];
while (reference.length > 1) {
reference = reference.substr(1);
record.product_reference_suffixes.push(reference);
}
return record;
});
index.saveObjects(objects).then(() => {
// done
});
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| with open('products.json') as f:
objects = json.load(f)
for obj in objects:
reference = obj['product_reference']
obj['product_reference_suffixes'] = []
while len(reference.decode('UTF-8')) > 1:
reference = reference[1:]
obj['product_reference_suffixes'].append(reference);
index.save_objects(objects, {
'autoGenerateObjectIDIfNotExist': True
})
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| let filePath = Bundle.main.path(forResource: "products", ofType: "json")!
let contentData = FileManager.default.contents(atPath: filePath)!
let records = try! JSONSerialization.jsonObject(with: contentData, options: []) as! [[String: Any]]
let objects = records.map { (record) -> [String : Any] in
var reference = record["product_reference"] as! String
var suffixes: [String] = []
while reference.count > 1 {
reference = reference.substring(from: reference.index(reference.startIndex, offsetBy: 1))
suffixes.append(reference)
}
var record = record
record["product_reference_suffixes"] = suffixes
return record
}
index.addObjects(objects)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| val string = File("products.json").readText()
val objects = Json.parse(JsonObjectSerializer.list, string)
val records = objects.map {
val map = it.toMutableMap()
val suffixes = mutableListOf<JsonElement>()
var word = map.getValue("product_reference").content
while (word.length > 1) {
word = word.substring(1)
suffixes += JsonLiteral(word)
}
map["product_reference_suffixes"] = JsonArray(suffixes)
JsonObject(map)
}
index.saveObjects(records)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| IEnumerable<JObject> records = JsonConvert.DeserializeObject<IEnumerable<JObject>>(File.ReadAllText("products.json"));
foreach (var record in records)
{
var word = record["product_reference"].ToString();
var suffixes = new JArray();
while (word.Length > 1)
{
word = word.Substring(1);
suffixes.Add(word);
}
record["product_reference_suffixes"] = suffixes;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| ObjectMapper objectMapper = Defaults.getObjectMapper();
InputStream input = new FileInputStream("products.json");
JsonNode[] records = objectMapper.readValue(input, JsonNode[].class);
for (JsonNode record : records) {
String word = record.get("product_reference").asText();
ArrayNode suffixes = objectMapper.createArrayNode();
while (word.length() > 1) {
word = word.substring(1);
suffixes.add(word);
}
((ObjectNode) record).put("product_reference_suffixes", suffixes);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| type Product struct {
ObjectID string `json:"objectID"`
ProductReference string `json:"product_reference"`
ProductReferenceSuffixes []string `json:"product_reference_suffixes"`
// Other fields
}
var products []Product
data, _ := ioutil.ReadFile("products.json")
_ = json.Unmarshal(data, &products)
for _, product := range products {
var suffixes []string
reference := product.ProductReference
for i := len(reference) - 1; i > 0; i-- {
suffixes = append(suffixes, reference[i:])
}
product.ProductReferenceSuffixes = suffixes
}
res, err := index.SaveObjects(products)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| import java.io.FileInputStream
import algolia.AlgoliaClient
import algolia.AlgoliaDsl._
import algolia.responses.ObjectID
import org.json4s._
import org.json4s.native.JsonMethods._
import scala.concurrent.{ExecutionContext, ExecutionContextExecutor}
case class Product(objectID: String,
name: String,
product_reference: String,
product_reference_suffixes: Seq[String],
price: Int,
popularity: Int) extends ObjectID
object Main {
def main(args: Array[String]): Unit = {
implicit val ec: ExecutionContextExecutor = ExecutionContext.global
val client = new AlgoliaClient("YourApplicationID", "YourWriteAPIKey")
val products =
parse(new FileInputStream("products.json"))
.extract[Seq[Product]]
.map(p => {
val suffixes = p.product_reference.tails.drop(1).toSeq
p.copy(product_reference_suffixes = suffixes)
})
client.execute {
index into "index" objects products
}
}
}
|
The processed records include a new attribute, product_reference_suffixes
:
1
2
3
4
5
6
7
8
9
10
11
12
| [
{
"name": "Apple iPhone 16",
"product_reference": "002ABCDEF",
"product_reference_suffixes": ["02ABCDEF", "2ABCDEF", "ABCDEF", "BCDEF", "CDEF", "DEF", "EF", "F"]
},
{
"name": "Apple iPhone 16 Max",
"product_reference": "001GHIJKL",
"product_reference_suffixes": ["01GHIJKL", "1GHIJKL", "GHIJKL", "HIJKL", "IJKL", "JKL", "KL", "L"]
}
]
|
Limitations
Generating a list of suffixes for each record can increase index size and slow performance,
It may also reduce relevance for short suffixes such as “EF” or “F”,
because these can match unrelated records with similar character sequences.
Use this approach only if you genuinely need to query in the middle or the end of words.
To reduce the impact of suffix generation, consider the following strategies:
- Generate suffixes only for strings longer than a defined length, such as 4 or more characters.
- Limit suffix generation to a specific number per record, such as 5.
- Apply suffix generation only to structured fields, such as product codes or identifiers.
Update your searchable attributes
Add product_reference_suffixes
to your searchable attributes to make the suffixes searchable.
1
2
3
4
5
6
7
8
9
10
11
12
| var response = await client.SetSettingsAsync(
"ALGOLIA_INDEX_NAME",
new IndexSettings
{
SearchableAttributes = new List<string>
{
"name",
"product_reference",
"product_reference_suffixes",
},
}
);
|
1
2
3
4
5
6
7
8
9
10
| final response = await client.setSettings(
indexName: "ALGOLIA_INDEX_NAME",
indexSettings: IndexSettings(
searchableAttributes: [
"name",
"product_reference",
"product_reference_suffixes",
],
),
);
|
1
2
3
4
5
6
7
8
| response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"ALGOLIA_INDEX_NAME",
search.NewEmptyIndexSettings().SetSearchableAttributes(
[]string{"name", "product_reference", "product_reference_suffixes"})))
if err != nil {
// handle the eventual error
panic(err)
}
|
1
2
3
4
| client.setSettings(
"ALGOLIA_INDEX_NAME",
new IndexSettings().setSearchableAttributes(Arrays.asList("name", "product_reference", "product_reference_suffixes"))
);
|
1
2
3
4
| const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { searchableAttributes: ['name', 'product_reference', 'product_reference_suffixes'] },
});
|
1
2
3
4
5
6
| var response = client.setSettings(
indexName = "ALGOLIA_INDEX_NAME",
indexSettings = IndexSettings(
searchableAttributes = listOf("name", "product_reference", "product_reference_suffixes"),
),
)
|
1
2
3
4
5
6
7
8
9
10
11
| $response = $client->setSettings(
'ALGOLIA_INDEX_NAME',
['searchableAttributes' => [
'name',
'product_reference',
'product_reference_suffixes',
],
],
);
|
1
2
3
4
5
6
7
8
9
10
| response = client.set_settings(
index_name="ALGOLIA_INDEX_NAME",
index_settings={
"searchableAttributes": [
"name",
"product_reference",
"product_reference_suffixes",
],
},
)
|
1
2
3
4
5
6
| response = client.set_settings(
"ALGOLIA_INDEX_NAME",
Algolia::Search::IndexSettings.new(
searchable_attributes: ["name", "product_reference", "product_reference_suffixes"]
)
)
|
1
2
3
4
5
6
7
8
9
| val response = Await.result(
client.setSettings(
indexName = "ALGOLIA_INDEX_NAME",
indexSettings = IndexSettings(
searchableAttributes = Some(Seq("name", "product_reference", "product_reference_suffixes"))
)
),
Duration(100, "sec")
)
|
1
2
3
4
5
6
7
8
| let response = try await client.setSettings(
indexName: "ALGOLIA_INDEX_NAME",
indexSettings: IndexSettings(searchableAttributes: [
"name",
"product_reference",
"product_reference_suffixes",
])
)
|
In your list of searchable attributes,
place product_reference_suffixes
below product_reference
to exact matches rank higher than partial matches.
See also