Ranking Requests

Goal: Promoted wants to rank content at the end of your getContent() API call.

Goal

In order to rank Content , your API server needs to call Promoted's Delivery API. A similar integration works both for (1) initial logging to Promoted, (2) running experiments and (3) fully launched with Promoted's rankings.

How to integrate?

The Promoted server-side SDK is called in your API server after ranked your own list of content. Click here for reasons about why we have a server-side SDK?

The main inputs to the SDKs are a list of Request and a list of Insertion candidates.

  • Request = A request for a list of content. E.g. search, feed, related items. Protocol Buffer definition
  • Insertion = A content candidate when processing a Request. We start with more potential Request Insertions and narrow down to a subset of Response Insertions to return to the UI. Insertions are different from Impressions because Insertions might not get viewed.

We have Delivery client libraries in multiple languages. More detailed integration instructions can be found in each SDK docs.

Here is an example code block for Typescript.

static async promotedDeliver(req: any, products: Product[], res: Response) {
  const responsePromise = promotedClient.deliver({
    // onlyLog: true - if you want to only log to Promoted.
    request: {
      userInfo: {
        anonUserId: req.anonUserId,
      },
      useCase: 'FEED', // Supports other enum values like 'SEARCH' and 'DISCOVER'.
      // TODO - add `query` for the search query.
      properties: {
        struct: {
          // TODO - Add user, request and context features.
          // TODO - Add request filters.  The properties are used to generate a paging key that is used for caching.
        }
      },
      insertion: products.map((product, retrievalRank) => ({
        contentId: product.id,
        retrievalRank,
        properties: {
          struct: {
            // TODO - add user-item features here.
            // Example: "numReviews": product.numReviews,
          },
        },
      })),
    },
  });
  // Construct the map while the RPC is happening.
  const productIdToProduct = products.reduce((map, product) => {
      map[product.id] = product;
      return map;
  }, {});
  const clientResponse = await responsePromise;
  const responseProducts = toContents<Product>(clientResponse.insertion, productIdToProduct);

  // Change the response Product list to use the values in the returned Insertions.
  sendSuccessToClient(res, { products: responseProducts) });
  // Do not block.  Log asynchronously.
  clientResponse.log().catch(handleError);
}

What data to pass on Request?

For Promoted to use data for ranking, the data needs to either be sent into the Delivery API call or it needs to be in Promoted already (e.g. previously sent content data through Content Service). This is where interaction features (combinations of content, user and context) should be passed in.

Example fields to pass in:

  • Your own retrieval scores.
  • Dynamic price.
  • Dynamic UI badges.

See How to send features to Promoted? for explanations on how to send data to Promoted.

Data that impacts ranking either needs to be passed in using:

  1. The Delivery API call.
  2. The Content Service (offline). This can be used for passing stable item or user data. It will

It’s possible to pass stable item or user features on this call but it’s better to pass those through the Content Service to improve scale and speed.

How does paging works?

Overview

  1. Clients do their own retrieval. They narrow many candidates down to the top N (e.g. 300-500) to send into Promoted's SDK. These few hundred candidates are called Request Insertions.
  2. Delivery SDK (either in the SDK or Delivery API) will narrow down to a smaller page of items. These are Response Insertions. They are specified using Request.paging.offset and Request.paging.size.

For this case:

  • paging.offset = 0- Indicates which position to return as the first insertion in the Response.insertion list.
  • paging.size = 50- The size of the response page size. Similar to limit in select statements.
  • retrievalInsertionOffset = 0 - This is described down below if you want to send a different subset of insertions to Promoted's SDK.

How are recently served response insertions cached?

When Delivery API receives a Request, the API looks in our page cache by a paging key. This allows Promoted to return already paged items back to the user if they request the same pages again.

Promoted creates the paging key from Request properties such as:

  • anonUserId
  • clientInfo
  • useCase
  • searchQuery
  • blenderConfig
  • properties.

Promoted also has a setting for how long to keep recent pages. This setting defaults to around 30 minutes.

🚧

If you have Request.properties that will change across page, contact Promoted to exclude them from the paging key

The default behavior is to include all Request.properties in the paging key. If a Request.property changes across pages (e.g. request timestamp), it'll change the paging key and break paging.

🚧

When a new page is requested from Delivery API, Promoted will try to serve the next best recommendations to that page, regardless of the page number

Example:

  • User starts their request on page 10. Promoted will serve the top results on page 10.
  • Then the user hops back to page 0. Promoted will serve the next best items on page 0.

The results for each page are temporarily cached inside Promoted's servers.

If users try to share links to result lists, the items will be different for each user that tries to load the list. Each user gets their own personalized results.

Promoted can customize paging behavior for your use case. E.g. how long to keep the paged results in the paging DB?

What happens to missing Request.insertion.content_ids on subsequent Requests?

When a user makes a repeated request and certain contentIds previously allocated are missing in the new Request.insertion list, the Delivery API's behavior depends on its configuration. This situation arises due to common issues like inconsistent retrieval results or changes in item details.

Configurations and Behaviors

  1. Default Setting (limitToRequestInsertions=false):

The Delivery API continues to return the previously allocated contentId in its original position, even if it's no longer in the current Request.insertion. This allows for expanding the list of contentIds across multiple requests.

Example:

  • First Request: contentIds [A, B, C] for positions 0-2. Response: [B, C, A].
  • Second Request: contentIds [C, D, E] for positions 0-2. Response: [B, C, A].
  1. Alternative Setting (limitToRequestInsertions=true):

Any missing Request.insertion.contentIds are omitted from the response. This limits the content to only those items currently listed in Request.insertion.

Example:

  • First Request: contentIds [A, B, C] for positions 0-2. Response: [B, C, A].
  • Second Request: contentIds [C, D, E] for positions 0-2. Response: [D, C, E]. Here, A and B are excluded as they are not in the current insertion list, while C retains its position and D and E fill the available slots.

How to send more insertions than the top few hundred?

📘

Contact Promoted engineers if you want to send additional Request Insertions to Delivery API

Different windows of retrieval candidates can be sent to Promoted's SDK. The window is specified using the retrieval_insertion_offset parameter. The union config option needs to be enabled.

For this case:

  • Request.paging.offset = 550- The starting index.
  • Request.paging.size = 50- The response page size
  • retrievalInsertionOffset = 500 - The starting index of the window of retrieval insertions being passed into the SDK.

Delivery API will cache bot the earlier range of[0, 500) insertions and the additional [500, 999)insertions.