Getting Started

Indx is a high-performance full-text search engine for JSON data. It uses pattern recognition and BM25 scoring — no tokenizers, stemmers, or analyzers to configure. It handles typos and messy input out of the box.

Two Ways to Run Indx

Embed it — the IndxSearchLib NuGet package. Search runs inside your .NET application; nothing else to deploy. Follow the embedded walkthrough below.

Host it — the Indx server: an HTTP API, a dashboard for teams and datasets, API keys, and an MCP endpoint. For any language (Node.js, Python, Java, …), for several applications sharing one index, or when a .NET team wants the dashboard and team management too. Follow the server walkthrough below.


Embedded Walkthrough (C#)

1. Install

bash
dotnet add package IndxSearchLib

2. Prepare Your Data

Indx accepts a JSON array of objects. Nested objects are supported; nested fields use dot notation.

json
[
  { "id": 1, "name": "Wireless Headphones", "brand": "Acme", "price": 79.99, "category": "audio" },
  { "id": 2, "name": "Bluetooth Speaker",   "brand": "Zync", "price": 49.99, "category": "audio" },
  { "id": 3, "name": "USB-C Hub",           "brand": "Acme", "price": 29.99, "category": "accessories" }
]

3. Init — Discover Fields

Init parses the JSON structure and returns the list of fields it discovers, along with each field's inferred type. The fields start unconfigured — you assign roles (searchable, filterable, sortable, facetable) in the next step.

csharp
using Indx.Api;

var engine = new SearchEngine();

using var stream = File.OpenRead("products.json");
engine.Init(stream);

After Init, call GetFieldConfiguration() to inspect the discovered fields:

csharp
foreach (var field in engine.GetFieldConfiguration())
    Console.WriteLine(
quot;{field.FieldName} type={field.FieldType}"); // id type=Number // name type=String // brand type=String // price type=Number // category type=String

4. Configure Fields

Nothing is searchable by default. Mark each field's role explicitly:

csharp
var name     = engine.GetField("name")!;
var brand    = engine.GetField("brand")!;
var price    = engine.GetField("price")!;
var category = engine.GetField("category")!;

name.Searchable  = true;
name.Weight      = 2.0f;   // higher weight = more influence on score

brand.Searchable  = true;
brand.Weight      = 1.0f;

price.Filterable = true;
price.Sortable   = true;

category.Filterable = true;
category.Facetable  = true;

Weight is a float (default 1.0, i.e. neutral). Higher means stronger influence on BM25 scoring relative to other searchable fields; below 1.0 de-emphasizes the field.

5. Load and Index

Stream the same JSON again for loading, then build the index:

csharp
stream.Position = 0;
engine.Load(stream);
engine.Index();

The engine is now in Ready state.

csharp
var result = engine.Search(new Query("wireless", 20));

foreach (var entry in result.Entries)
    Console.WriteLine(
quot;key={entry.DocumentKey} score={entry.Score}");

To retrieve the full JSON of the matching documents:

csharp
var keys = result.Entries.Select(e => e.DocumentKey).ToArray();
var docs = engine.GetJsonDataOfKeys(keys);

7. Add a Filter

Filters are evaluated server-side against the full dataset — never filter the result array yourself.

csharp
// Match only the "audio" category
var audioFilter = engine.CreateValueFilter("category", "audio", out _);

var query = new Query("wireless", 20) { Filter = audioFilter };
var result = engine.Search(query);

For numeric ranges:

csharp
var affordable = engine.CreateRangeFilter("price", 0, 50, out _);

Combine with AND or OR:

csharp
var combined = audioFilter & affordable; // AND; use | for OR, ! to negate

8. Sort Results

Sorting is secondary to score. However, an empty search (Query.Text = "", see Empty Search) results in all scores being 0, in which case the sort order behaves as primary.

csharp
var query = new Query("", 20) { SortBy = "price", SortAscending = true };

9. Facets

Facets return distinct value counts for a field — useful for sidebar filters.

csharp
var query = new Query("wireless", 20) { EnableFacets = true };
var result = engine.Search(query);

foreach (var (value, count) in result.Facets["category"])
    Console.WriteLine(
quot;{value}: {count}"); // audio: 2 // accessories: 0

Server Walkthrough (HTTP)

Run the server:

bash
git clone https://github.com/indxSearch/Indx
cd Indx
dotnet run

Requires .NET 10 SDK. See Server Setup for registration, API keys, and deployment.

Load Data

bash
# Analyze structure
curl -X POST https://localhost:5001/api/teams/acme/datasets/products/analyze \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  --data-binary @products.json

# Configure fields (204 No Content)
curl -X PUT https://localhost:5001/api/teams/acme/datasets/products/fields/configuration \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '[
    { "fieldName": "name",     "searchable": true, "weight": 2.0 },
    { "fieldName": "brand",    "searchable": true, "weight": 1.0 },
    { "fieldName": "price",    "filterable": true, "sortable": true },
    { "fieldName": "category", "filterable": true, "facetable": true }
  ]'

# Load documents (204 No Content)
curl -X POST https://localhost:5001/api/teams/acme/datasets/products/load \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  --data-binary @products.json

# Build index — returns 202 Accepted; the build runs in the background
curl -X POST https://localhost:5001/api/teams/acme/datasets/products/index \
  -H "Authorization: Bearer <token>"

# Poll status until systemState is 4 (Ready)
curl https://localhost:5001/api/teams/acme/datasets/products/status \
  -H "Authorization: Bearer <token>"
bash
curl -X POST https://localhost:5001/api/teams/acme/datasets/products/search \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "text": "wireless", "maxNumberOfRecordsToReturn": 20 }'
bash
# Create a filter
curl -X POST https://localhost:5001/api/teams/acme/datasets/products/filters/value \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "fieldName": "category", "value": "audio" }'
# → { "hashString": "abc123..." }

# Use it in search
curl -X POST https://localhost:5001/api/teams/acme/datasets/products/search \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "text": "wireless", "maxNumberOfRecordsToReturn": 20, "filter": { "hashString": "abc123..." } }'

Next Steps