Search code examples
c#azureindexingazure-sql-databaseazure-cognitive-search

Is it possible to populate a collection in an Azure Search index?


I've been currently working with Azure Search, and I stumbled upon a scenario where I have two tables: one describes a place, and the other has multiple photos for each place.

I wanted to use two indexers to populate the same index, where one would populate the place's details (name, coordinates, etc), while the other would get the photos, run a custom WebApiSkill that gets certain details from the photo, and add them to a complex collection on the index.

The issue is, I can't figure out how to populate that photo collection on the index. I have tried defining my outputFieldMappings as Photos/*/Details, or just Photos/Details, but I keep getting the same error:

Microsoft.Rest.Azure.CloudException: 'Output field mapping specifies target field 'Photos/*/PhotoURI' that doesn't exist in the index'

Considering this, I would like to know if it is at all possible to populate a complex collection like this in Azure Search, and if it is, how would I go about doing it?


Tables:

CREATE TABLE Places
(
    Id             bigint         not null  IDENTITY(1,1)  PRIMARY KEY,
    Name           nvarchar(50)   not null,
    Location       nvarchar(50)   not null
);

CREATE TABLE PlacePhotos
(
    PhotoURI  nvarchar(200)  not null  PRIMARY KEY,
    PlaceId   bigint         not null  REFERENCES Places (Id)
);

Classes used for creating the index:

public partial class Photo
{
    [IsSearchable]
    public string PhotoURI { get; set; }

    public List<Details> Details { get; set; }
}
public partial class Place
{
    [System.ComponentModel.DataAnnotations.Key]
    public string Id { get; set; }

    [IsSearchable, IsFilterable, IsSortable, IsFacetable]
    public string Name { get; set; } 

    [IsSearchable, IsFilterable]
    public string Location { get; set; }

    [IsSearchable, IsFilterable, IsSortable]
    public List<Photo> Photos { get; set; }

}

Indexer (with the PlacePhotos table as its data source):

private static void Create_PlacePhotos_Indexer(SearchServiceClient serviceClient)
{
    var indexerDef = new Indexer(
        name: placePhotosIndexer,
        dataSourceName: placePhotosDataSource,
        targetIndexName: index,
        skillsetName: skillset,
        schedule: new IndexingSchedule
        {
            Interval = new TimeSpan(0, 30, 0)
        },
        fieldMappings: new List<FieldMapping>
        {
            new FieldMapping("PlaceId", "Id"),
            new FieldMapping("PhotoURI", "PhotoURI")
        },
        outputFieldMappings: new List<FieldMapping>
        {
            new FieldMapping("/document/Id", "Id"),
            new FieldMapping("/document/PhotoURI", "Photos/*/PhotoURI"),
            new FieldMapping("/document/Details", "Photos/*/Details")
        }
    );
    serviceClient.Indexers.CreateOrUpdate(indexerDef);
}

Solution

  • Output field mapping target field names must be top-level index field names, so Photos/*/PhotoURI is not valid and you have to target Photos directly.

    To "shape" the Photos value to be mapped, you can use the ShaperSkill

    {
      "@odata.type": "#Microsoft.Skills.Util.ShaperSkill",
      "context": "/document/photos/*",
      "inputs": [
        {
          "name": "PhotoURI",
          "source": "/document/photos/*/PhotoURI"
        },
        {
          "name": "Details",
          "source": "/document/photos/*/Details"
        }
      ],
      "outputs": [
        {
          "name": "output",
          "targetName": "shapedValue"
        }
      ]
    }
    

    and output field mapping

    new FieldMapping("/document/photos/*/shapedValue", "Photos")
    

    NOTE - you might have to rearrange your skills that output URI and details, so they share a common context. From your snippet above it looks like each document has only 1 photo, which is different from your index definition.