> ## Documentation Index
> Fetch the complete documentation index at: https://algolia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom ranking of results per category

> Adjust custom rankings per category.

export const Records = () => <Tooltip tip="A record is a searchable object in an Algolia index. Each record consists of named attributes." cta="Algolia records" href="/doc/guides/sending-and-managing-data/prepare-your-data#algolia-records">
    records
  </Tooltip>;

export const Index = () => <Tooltip tip="An Algolia index is a searchable dataset that consists of records and configuration settings. These settings define how the records are searched and ranked.">
    index
  </Tooltip>;

export const Filter = () => <Tooltip tip="A filter is a condition that limits which records Algolia returns. Filters often use one or more facet-value pairs, such as brand:Apple AND color:red. You can also filter by numeric values, dates, tags, booleans, or geographic constraints." cta="Filtering" href="/doc/guides/managing-results/refine-results/faceting">
    filter
  </Tooltip>;

When you set [custom ranking](/doc/guides/managing-results/must-do/custom-ranking) attributes in Algolia,
they affect the whole <Index /> (independent of categories).
But what if you want to use different custom rankings per category?
This guide shows how you can use duplicate <Records /> in the Algolia index and use a <Filter /> to apply different custom rankings per category.

## Implementation steps

To use different custom rankings per category, you need to perform these tasks:

* **Data**: use duplicate records for each product, with extra attributes for "ranked category," and "rank within that category"
* **Index**: select the "rank within category" attribute for custom ranking and select the "ranked category" attribute for faceting. To handle duplicate search results, use a distinct identifier for each product
* **Frontend**: dynamically filter search results with or without category filters

## Prepare your data

**Duplicate records** let you use filters to rank products differently in different categories. Each record has attributes for the category in which the product ranks and the rank within that category.

For example, assume you have an online clothing store. One of your products is Sports shoes, and you want to rank these shoes higher as users search in narrower categories. For each record, you add new attributes:

* `category`: a list with all category names for this product—ranked and not ranked. This is used for [faceting](/doc/guides/managing-results/refine-results/faceting) the search results. See [Creating a category data attribute](/doc/guides/solutions/ecommerce/browse/tutorials/category-pages) for more information, and how to implement hierarchical categories.
* `ranked_category`: the name of the category for which the product ranks. To show a product also for searches without category filters, **one record per product must have `ranked_category: none`**.
* `category_rank`: the rank of the product in the category. Each duplicate of the product's record has one corresponding pair of `ranked_category` and `category_rank` attributes, which enables the per-category custom ranking.
* `parent_objectID`: a unique identifier for events. When [tracking click and conversion events](/doc/guides/sending-events) with the Insights API, you use this identifier to link all events from the duplicate records to the same product.

Conceptually, users search for sports shoes in three different ways:

* Search "Sports shoes" without any category filters
* Search "Sports shoes" in the category Shoes
* Search "Sports shoes" in the category Shoes > Sports

If you want to use different custom rankings per category, you need at least three records per product.

* **Main record for searches without categories and all other categories**.

  This record matches when users search without category filters or in other categories,
  where this product doesn't rank.
  The textual and custom relevance settings for the index decide the position in the search results.
  When tracking click and conversion events for this record, use the `objectID` attribute as the unique identifier.

  ```json JSON icon=braces theme={"system"}
  {
    "objectID": "492533",
    "sku": "4597310",
    "name": "Sports shoes",
    "category": ["Shoes", "Shoes > Sports"],
    "ranked_category": "none",
    "sales_count": 852
  }
  ```

* **Record for searches in the *Shoes* category**.

  When users search in the *Shoes* category,
  this product shows at the custom rank 20.
  To mark this record as a duplicate of the first record,
  the attribute `parent_objectID` has the same value as the `objectID` of the first record.
  For tracking events, use the `parent_objectID` attribute as the unique identifier.

  ```json JSON icon=braces theme={"system"}
  {
    "objectID": "473828",
    "parent_objectID": "492533",
    "sku": "4597310",
    "name": "Sports shoes",
    "category": ["Shoes", "Shoes > Sports"],
    "ranked_category": "Shoes",
    "category_rank": 20,
    "sales_count": 852
  }
  ```

* **Record for searches in the *Shoes > Sports* category**.

  When users search in the *Shoes* > *Sports* category,
  this product shows at the top of the custom ranking.
  To track events for this record as duplicates of the first record,
  use the attribute `parent_objectID`.
  It has the same value as the `objectID` attribute of the first record.

  ```json JSON icon=braces theme={"system"}
  {
    "objectID": "511621",
    "parent_objectID": "492533",
    "sku": "4597310",
    "name": "Sports shoes",
    "category": ["Shoes", "Shoes > Sports"],
    "ranked_category": "Shoes > Sports",
    "category_rank": 1,
    "sales_count": 852
  }
  ```

## Configure the index

To configure the Algolia index for category-based custom ranking, follow these steps:

1. Add the `category_rank` attribute to the top of the custom ranking criteria and sort the results by ascending values so that low values rank high.
   In the [Algolia dashboard](https://dashboard.algolia.com/explorer/configuration/),
   choose your index and select **Ranking and Sorting**.
2. Add `ranked_category` to the attributes for faceting.
   Since you want to use this attribute just for filtering the search results,
   use the [`filterOnly`](/doc/api-reference/api-parameters/attributesForFaceting#modifiers) modifier to discard the facet values and counts. This reduces the size of the index and speeds up the search. In the Dashboard, go to **Facets** and add the `ranked_category` attribute.
3. When searching with category filters, the search might return multiple records for the same product.
   That's why you need to deduplicate the results.
   In the dashboard, go to **Deduplication and Grouping** and set **Distinct** to **true**.
   Select the `sku` attribute as **Attribute for Distinct**.

<Info>
  Using `distinct` is computationally intensive and can slow down the search.
</Info>

You can configure [sorting attributes](/doc/guides/managing-results/refine-results/sorting) independently from the custom ranking attributes per category.
Both settings don't influence each other.

For more information, see:

* [Create custom ranking attributes](/doc/guides/managing-results/must-do/custom-ranking/how-to/configure-custom-ranking)
* [Declare attributes for faceting with the API](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting)
* [`attributeForDistinct`](/doc/api-reference/api-parameters/attributeForDistinct)
* [`distinct`](/doc/api-reference/api-parameters/distinct)

## Show custom ranking per category in your frontend

To take advantage of the per-category custom ranking in the frontend,
you need to filter the search results on the `ranked_category` attribute.
Depending on whether users search with or without categories, you need to filter the results dynamically:

* When users search **without categories**:

<CodeGroup>
  ```cs C# theme={"system"}
  var response = await client.SearchSingleIndexAsync<Hit>(
    "INDEX_NAME",
    new SearchParams(
      new SearchParamsObject
      {
        Query = "User search query",
        FacetingAfterDistinct = true,
        Filters = "ranked_category:none",
      }
    )
  );
  ```

  ```dart Dart theme={"system"}
  final response = await client.searchSingleIndex(
    indexName: "INDEX_NAME",
    searchParams: SearchParamsObject(
      query: "User search query",
      facetingAfterDistinct: true,
      filters: "ranked_category:none",
    ),
  );
  ```

  ```go Go theme={"system"}
  response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
    "INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
    search.NewEmptySearchParamsObject().SetQuery("User search query").SetFacetingAfterDistinct(true).SetFilters("ranked_category:none"))))
  if err != nil {
    // handle the eventual error
    panic(err)
  }
  ```

  ```java Java theme={"system"}
  SearchResponse response = client.searchSingleIndex(
    "INDEX_NAME",
    new SearchParamsObject().setQuery("User search query").setFacetingAfterDistinct(true).setFilters("ranked_category:none"),
    Hit.class
  );
  ```

  ```js JavaScript theme={"system"}
  const response = await client.searchSingleIndex({
    indexName: 'indexName',
    searchParams: { query: 'User search query', facetingAfterDistinct: true, filters: 'ranked_category:none' },
  });
  ```

  ```kotlin Kotlin theme={"system"}
  var response =
    client.searchSingleIndex(
      indexName = "INDEX_NAME",
      searchParams =
        SearchParamsObject(
          query = "User search query",
          facetingAfterDistinct = true,
          filters = "ranked_category:none",
        ),
    )
  ```

  ```php PHP theme={"system"}
  $response = $client->searchSingleIndex(
      'INDEX_NAME',
      ['query' => 'User search query',
          'facetingAfterDistinct' => true,
          'filters' => 'ranked_category:none',
      ],
  );
  ```

  ```python Python theme={"system"}
  response = client.search_single_index(
      index_name="INDEX_NAME",
      search_params={
          "query": "User search query",
          "facetingAfterDistinct": True,
          "filters": "ranked_category:none",
      },
  )
  ```

  ```ruby Ruby theme={"system"}
  response = client.search_single_index(
    "INDEX_NAME",
    Algolia::Search::SearchParamsObject.new(
      query: "User search query",
      faceting_after_distinct: true,
      filters: "ranked_category:none"
    )
  )
  ```

  ```scala Scala theme={"system"}
  val response = Await.result(
    client.searchSingleIndex(
      indexName = "INDEX_NAME",
      searchParams = Some(
        SearchParamsObject(
          query = Some("User search query"),
          facetingAfterDistinct = Some(true),
          filters = Some("ranked_category:none")
        )
      )
    ),
    Duration(100, "sec")
  )
  ```

  ```swift Swift theme={"system"}
  let response: SearchResponse<Hit> = try await client.searchSingleIndex(
      indexName: "INDEX_NAME",
      searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(
          query: "User search query",
          filters: "ranked_category:none",
          facetingAfterDistinct: true
      ))
  )
  ```
</CodeGroup>

For searches without categories, the results show records with the `ranked_category: none` attribute.

* When users search **with categories**, there are two possibilities:
  the product ranks for the current category, or it doesn't (but it might rank for a different category):

<CodeGroup>
  ```cs C# theme={"system"}
  var response = await client.SearchSingleIndexAsync<Hit>(
    "INDEX_NAME",
    new SearchParams(
      new SearchParamsObject
      {
        Query = "User search query",
        FacetingAfterDistinct = true,
        Filters =
          "category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)",
      }
    )
  );
  ```

  ```dart Dart theme={"system"}
  final response = await client.searchSingleIndex(
    indexName: "INDEX_NAME",
    searchParams: SearchParamsObject(
      query: "User search query",
      facetingAfterDistinct: true,
      filters:
          "category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)",
    ),
  );
  ```

  ```go Go theme={"system"}
  response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
    "INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
    search.NewEmptySearchParamsObject().
      SetQuery("User search query").
      SetFacetingAfterDistinct(true).
      SetFilters("category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)"),
  )))
  if err != nil {
    // handle the eventual error
    panic(err)
  }
  ```

  ```java Java theme={"system"}
  SearchResponse response = client.searchSingleIndex(
    "INDEX_NAME",
    new SearchParamsObject()
      .setQuery("User search query")
      .setFacetingAfterDistinct(true)
      .setFilters("category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR" + " ranked_category:none)"),
    Hit.class
  );
  ```

  ```js JavaScript theme={"system"}
  const response = await client.searchSingleIndex({
    indexName: 'indexName',
    searchParams: {
      query: 'User search query',
      facetingAfterDistinct: true,
      filters: 'category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)',
    },
  });
  ```

  ```kotlin Kotlin theme={"system"}
  var response =
    client.searchSingleIndex(
      indexName = "INDEX_NAME",
      searchParams =
        SearchParamsObject(
          query = "User search query",
          facetingAfterDistinct = true,
          filters =
            "category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)",
        ),
    )
  ```

  ```php PHP theme={"system"}
  $response = $client->searchSingleIndex(
      'INDEX_NAME',
      ['query' => 'User search query',
          'facetingAfterDistinct' => true,
          'filters' => 'category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)',
      ],
  );
  ```

  ```python Python theme={"system"}
  response = client.search_single_index(
      index_name="INDEX_NAME",
      search_params={
          "query": "User search query",
          "facetingAfterDistinct": True,
          "filters": "category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)",
      },
  )
  ```

  ```ruby Ruby theme={"system"}
  response = client.search_single_index(
    "INDEX_NAME",
    Algolia::Search::SearchParamsObject.new(
      query: "User search query",
      faceting_after_distinct: true,
      filters: "category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)"
    )
  )
  ```

  ```scala Scala theme={"system"}
  val response = Await.result(
    client.searchSingleIndex(
      indexName = "INDEX_NAME",
      searchParams = Some(
        SearchParamsObject(
          query = Some("User search query"),
          facetingAfterDistinct = Some(true),
          filters =
            Some("category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)")
        )
      )
    ),
    Duration(100, "sec")
  )
  ```

  ```swift Swift theme={"system"}
  let response: SearchResponse<Hit> = try await client.searchSingleIndex(
      indexName: "INDEX_NAME",
      searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(
          query: "User search query",
          filters: "category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)",
          facetingAfterDistinct: true
      ))
  )
  ```
</CodeGroup>

If a product ranks for the current category, the search would return both records.
For example, if users search in the category Shoes, these records match the filter:

* `category: Shoes AND ranked_category: Shoes`
* `category: Shoes AND ranked_category: none`

Since you use `distinct` for this index, results only show the record with `ranked_category: Shoes`.

If users search in a different category, for example,
the category *Accessories*, the results show the record matching `category: Accessories and ranked_category: none`.

These rules decide the ranking between records with and without ranked categories:

1. Both records are the same except for the `ranked_category` and `category_rank` attributes.
   Since they have the same textual relevance,
   Algolia ranks them by their custom ranking attributes.
2. The first custom ranking attribute is `category_rank`.
   As the record with `ranked_category:none` doesn't have a `category_rank` attribute,
   it ranks lower than a record with a value for this attribute.
3. With `distinct(true)`, the search returns the first record from a list of identical records.
   Because you set `attributeForDistinct` to `sku`, all products with the same `sku` attribute are identical.

By default, faceting is applied before the deduplication.
To ensure correct facet counts,
deduplicate and then set [`facetingAfterDistinct`](/doc/api-reference/api-parameters/facetingAfterDistinct) to `true`.
You have to apply this setting at query time.
