본문 바로가기
WIKI 기술 지식 베이스

Nested field search

원문 보기 위키 갱신

출처: 문서

본문

중첩 필드 검색

벡터 인덱스에서 중첩 필드를 사용하면 하나의 문서에 여러 개의 벡터를 저장할 수 있어요. 예를 들어 문서가 다양한 구성 요소로 이루어져 있다면, 각 구성 요소마다 벡터 값을 생성하고 각 벡터를 중첩 필드에 저장할 수 있어요.

벡터 검색은 필드 수준에서 동작해요. 중첩 필드가 있는 문서의 경우, OpenSearch는 쿼리 벡터에 가장 가까운 벡터만 검사해서 그 문서를 결과에 포함할지 여부를 결정해요. 예를 들어 문서 A와 B를 담고 있는 인덱스를 생각해 볼게요. 문서 A는 벡터 A1과 A2로 표현되고, 문서 B는 벡터 B1로 표현돼요. 그리고 쿼리 Q에 대한 유사도 순서는 A1, A2, B1이에요. k 값을 2로 설정해서 쿼리 Q로 검색하면, 문서 A만 반환되는 게 아니라 문서 A와 B가 모두 반환돼요.

근사 검색(approximate search)의 경우 결과는 근사값이지 정확한 일치가 아니라는 점에 유의하세요.

중첩 필드를 사용한 벡터 검색은 Lucene과 Faiss 엔진에서 HNSW 알고리즘을 통해 지원돼요.

중첩 필드 인덱싱과 검색

중첩 필드를 사용한 벡터 검색을 하려면 index.knn을 true로 설정해서 벡터 인덱스를 만들어야 해요. type을 nested로 설정해서 중첩 필드를 만들고, 그 중첩 필드 안에 knn_vector 데이터 타입의 필드를 하나 이상 지정하세요. 이 예제에서는 knn_vector 필드인 my_vector가 nested_field 필드 안에 중첩되어 있어요:

PUT my-knn-index-1
{
  "settings": {
    "index": {
      "knn": true
    }
  },
  "mappings": {
    "properties": {
      "nested_field": {
        "type": "nested",
        "properties": {
          "my_vector": {
            "type": "knn_vector",
            "dimension": 3,
            "space_type": "l2",
            "method": {
              "name": "hnsw",
              "engine": "lucene",
              "parameters": {
                "ef_construction": 100,
                "m": 16
              }
            }
          },
          "color": {
            "type": "text",
            "index": false
          }
        }
      }
    }
  }
}

{% include copy-curl.html %}

인덱스를 만든 후에는 데이터를 좀 추가해 보세요:

PUT _bulk?refresh=true
{ "index": { "_index": "my-knn-index-1", "_id": "1" } }
{"nested_field":[{"my_vector":[1,1,1], "color": "blue"},{"my_vector":[2,2,2], "color": "yellow"},{"my_vector":[3,3,3], "color": "white"}]}
{ "index": { "_index": "my-knn-index-1", "_id": "2" } }
{"nested_field":[{"my_vector":[10,10,10], "color": "red"},{"my_vector":[20,20,20], "color": "green"},{"my_vector":[30,30,30], "color": "black"}]}

{% include copy-curl.html %}

그다음 knn 쿼리 타입을 사용해서 데이터에 벡터 검색을 실행해 보세요:

GET my-knn-index-1/_search
{
  "query": {
    "nested": {
      "path": "nested_field",
      "query": {
        "knn": {
          "nested_field.my_vector": {
            "vector": [1,1,1],
            "k": 2
          }
        }
      }
    }
  }
}

{% include copy-curl.html %}

쿼리 벡터에 가장 가까운 세 벡터가 모두 문서 1에 들어 있음에도 불구하고, k가 2로 설정되어 있기 때문에 쿼리는 문서 1과 2를 모두 반환해요:

{
  "took": 5,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 1.0,
    "hits": [
      {
        "_index": "my-knn-index-1",
        "_id": "1",
        "_score": 1.0,
        "_source": {
          "nested_field": [
            {
              "my_vector": [
                1,
                1,
                1
              ],
              "color": "blue"
            },
            {
              "my_vector": [
                2,
                2,
                2
              ],
              "color": "yellow"
            },
            {
              "my_vector": [
                3,
                3,
                3
              ],
              "color": "white"
            }
          ]
        }
      },
      {
        "_index": "my-knn-index-1",
        "_id": "2",
        "_score": 0.0040983604,
        "_source": {
          "nested_field": [
            {
              "my_vector": [
                10,
                10,
                10
              ],
              "color": "red"
            },
            {
              "my_vector": [
                20,
                20,
                20
              ],
              "color": "green"
            },
            {
              "my_vector": [
                30,
                30,
                30
              ],
              "color": "black"
            }
          ]
        }
      }
    ]
  }
}

Inner hits

중첩 필드에서의 매칭을 기준으로 문서를 조회할 때, 기본적으로는 응답에 어떤 내부 객체가 쿼리와 매칭됐는지에 대한 정보가 포함되지 않아요. 그래서 그 문서가 왜 매칭됐는지 알기 어려워요. 응답에 매칭된 중첩 필드에 대한 정보를 포함하려면 쿼리에 inner_hits 객체를 지정하면 돼요. inner_hits 안에서 매칭된 문서의 특정 필드만 반환하려면 fields 배열에 문서 필드를 지정하세요. 일반적으로 전체 문서가 반환되는 것을 피하려고 결과에서 _source도 함께 제외하는 게 좋아요. 다음 예제는 nested_field의 color 내부 필드만 반환해요:

GET my-knn-index-1/_search
{
  "_source": false,
  "query": {
    "nested": {
      "path": "nested_field",
      "query": {
        "knn": {
          "nested_field.my_vector": {
            "vector": [1,1,1],
            "k": 2
          }
        }
      },
      "inner_hits": {
        "_source": false,
        "fields":["nested_field.color"]
      }
    }
  }
}

{% include copy-curl.html %}

응답에는 매칭된 문서들이 담겨 있어요. 각 매칭 문서마다 inner_hits 객체는 fields 배열에 매칭된 문서의 nested_field.color 필드만 담고 있어요:

{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 1.0,
    "hits": [
      {
        "_index": "my-knn-index-1",
        "_id": "1",
        "_score": 1.0,
        "inner_hits": {
          "nested_field": {
            "hits": {
              "total": {
                "value": 1,
                "relation": "eq"
              },
              "max_score": 1.0,
              "hits": [
                {
                  "_index": "my-knn-index-1",
                  "_id": "1",
                  "_nested": {
                    "field": "nested_field",
                    "offset": 0
                  },
                  "_score": 1.0,
                  "fields": {
                    "nested_field.color": [
                      "blue"
                    ]
                  }
                }
              ]
            }
          }
        }
      },
      {
        "_index": "my-knn-index-1",
        "_id": "2",
        "_score": 0.0040983604,
        "inner_hits": {
          "nested_field": {
            "hits": {
              "total": {
                "value": 1,
                "relation": "eq"
              },
              "max_score": 0.0040983604,
              "hits": [
                {
                  "_index": "my-knn-index-1",
                  "_id": "2",
                  "_nested": {
                    "field": "nested_field",
                    "offset": 0
                  },
                  "_score": 0.0040983604,
                  "fields": {
                    "nested_field.color": [
                      "red"
                    ]
                  }
                }
              ]
            }
          }
        }
      }
    ]
  }
}

모든 중첩 히트 조회

기본적으로 중첩 필드를 쿼리할 때는 점수가 가장 높은 중첩 문서만 고려돼요. 각 부모 문서 안에 있는 모든 중첩 필드 문서의 점수를 조회하려면 쿼리에서 expand_nested_docs를 true로 설정하세요. 부모 문서의 점수는 이 점수들의 평균으로 계산돼요. 중첩 필드 문서들 중 가장 높은 점수를 부모 문서의 점수로 사용하려면 score_mode를 max로 설정하세요:

GET my-knn-index-1/_search
{
  "_source": false,
  "query": {
    "nested": {
      "path": "nested_field",
      "query": {
        "knn": {
          "nested_field.my_vector": {
            "vector": [1,1,1],
            "k": 2,
            "expand_nested_docs": true
          }
        }
      },
      "inner_hits": {
        "_source": false,
        "fields":["nested_field.color"]
      },
      "score_mode": "max"
    }
  }
}

{% include copy-curl.html %}

응답에는 매칭된 모든 문서가 담겨 있어요:

{
  "took": 13,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 1.0,
    "hits": [
      {
        "_index": "my-knn-index-1",
        "_id": "1",
        "_score": 1.0,
        "inner_hits": {
          "nested_field": {
            "hits": {
              "total": {
                "value": 3,
                "relation": "eq"
              },
              "max_score": 1.0,
              "hits": [
                {
                  "_index": "my-knn-index-1",
                  "_id": "1",
                  "_nested": {
                    "field": "nested_field",
                    "offset": 0
                  },
                  "_score": 1.0,
                  "fields": {
                    "nested_field.color": [
                      "blue"
                    ]
                  }
                },
                {
                  "_index": "my-knn-index-1",
                  "_id": "1",
                  "_nested": {
                    "field": "nested_field",
                    "offset": 1
                  },
                  "_score": 0.25,
                  "fields": {
                    "nested_field.color": [
                      "blue"
                    ]
                  }
                },
                {
                  "_index": "my-knn-index-1",
                  "_id": "1",
                  "_nested": {
                    "field": "nested_field",
                    "offset": 2
                  },
                  "_score": 0.07692308,
                  "fields": {
                    "nested_field.color": [
                      "white"
                    ]
                  }
                }
              ]
            }
          }
        }
      },
      {
        "_index": "my-knn-index-1",
        "_id": "2",
        "_score": 0.0040983604,
        "inner_hits": {
          "nested_field": {
            "hits": {
              "total": {
                "value": 3,
                "relation": "eq"
              },
              "max_score": 0.0040983604,
              "hits": [
                {
                  "_index": "my-knn-index-1",
                  "_id": "2",
                  "_nested": {
                    "field": "nested_field",
                    "offset": 0
                  },
                  "_score": 0.0040983604,
                  "fields": {
                    "nested_field.color": [
                      "blue"
                    ]
                  }
                },
                {
                  "_index": "my-knn-index-1",
                  "_id": "2",
                  "_nested": {
                    "field": "nested_field",
                    "offset": 1
                  },
                  "_score": 9.2250924E-4,
                  "fields": {
                    "nested_field.color": [
                      "yellow"
                    ]
                  }
                },
                {
                  "_index": "my-knn-index-1",
                  "_id": "2",
                  "_nested": {
                    "field": "nested_field",
                    "offset": 2
                  },
                  "_score": 3.9619653E-4,
                  "fields": {
                    "nested_field.color": [
                      "white"
                    ]
                  }
                }
              ]
            }
          }
        }
      }
    ]
  }
}

중첩 필드에 필터를 적용한 벡터 검색

중첩 필드를 사용한 벡터 검색에 필터를 적용할 수 있어요. 필터는 최상위 필드에 적용할 수도 있고, 중첩 필드 내부의 필드에 적용할 수도 있어요.

다음 예제는 최상위 필드에 필터를 적용해요.

먼저 중첩 필드가 있는 벡터 인덱스를 만들어 보세요:

PUT my-knn-index-1
{
  "settings": {
    "index": {
      "knn": true
    }
  },
  "mappings": {
    "properties": {
      "nested_field": {
        "type": "nested",
        "properties": {
          "my_vector": {
            "type": "knn_vector",
            "dimension": 3,
            "space_type": "l2",
            "method": {
              "name": "hnsw",
              "engine": "lucene",
              "parameters": {
                "ef_construction": 100,
                "m": 16
              }
            }
          }
        }
      }
    }
  }
}

{% include copy-curl.html %}

인덱스를 만든 후에는 데이터를 좀 추가해 보세요:

PUT _bulk?refresh=true
{ "index": { "_index": "my-knn-index-1", "_id": "1" } }
{"parking": false, "nested_field":[{"my_vector":[1,1,1]},{"my_vector":[2,2,2]},{"my_vector":[3,3,3]}]}
{ "index": { "_index": "my-knn-index-1", "_id": "2" } }
{"parking": true, "nested_field":[{"my_vector":[10,10,10]},{"my_vector":[20,20,20]},{"my_vector":[30,30,30]}]}
{ "index": { "_index": "my-knn-index-1", "_id": "3" } }
{"parking": true, "nested_field":[{"my_vector":[100,100,100]},{"my_vector":[200,200,200]},{"my_vector":[300,300,300]}]}

{% include copy-curl.html %}

그다음 필터와 함께 knn 쿼리 타입을 사용해서 데이터에 벡터 검색을 실행해 보세요. 다음 쿼리는 parking 필드가 true로 설정된 문서를 반환해요:

GET my-knn-index-1/_search
{
  "query": {
    "nested": {
      "path": "nested_field",
      "query": {
        "knn": {
          "nested_field.my_vector": {
            "vector": [
              1,
              1,
              1
            ],
            "k": 3,
            "filter": {
              "term": {
                "parking": true
              }
            }
          }
        }
      }
    }
  }
}

{% include copy-curl.html %}

쿼리 벡터에 가장 가까운 세 벡터가 모두 문서 1에 들어 있음에도 불구하고, 문서 1이 필터링되어 제외되기 때문에 쿼리는 문서 2와 3을 반환해요:

{
  "took": 10,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 0.0040983604,
    "hits": [
      {
        "_index": "my-knn-index-1",
        "_id": "2",
        "_score": 0.0040983604,
        "_source": {
          "parking": true,
          "nested_field": [
            {
              "my_vector": [
                10,
                10,
                10
              ]
            },
            {
              "my_vector": [
                20,
                20,
                20
              ]
            },
            {
              "my_vector": [
                30,
                30,
                30
              ]
            }
          ]
        }
      },
      {
        "_index": "my-knn-index-1",
        "_id": "3",
        "_score": 3.400898E-5,
        "_source": {
          "parking": true,
          "nested_field": [
            {
              "my_vector": [
                100,
                100,
                100
              ]
            },
            {
              "my_vector": [
                200,
                200,
                200
              ]
            },
            {
              "my_vector": [
                300,
                300,
                300
              ]
            }
          ]
        }
      }
    ]
  }
}

더 알아보기 (Learn more)