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

Semantic search

원문 보기 위키 갱신

출처: 문서

본문

시맨틱 검색

시맨틱 검색은 쿼리의 문맥과 의도를 고려해요. OpenSearch에서 시맨틱 검색은 텍스트 임베딩 모델을 통해 구현돼요. 시맨틱 검색은 밀집 벡터(float 값의 목록)를 생성하고 데이터를 벡터 인덱스에 수집해요.

사전 준비 사항
시맨틱 검색을 사용하기 전에 텍스트 임베딩 모델을 설정해야 해요. 자세한 내용은 모델 선택하기를 참고하세요. {: .note}

시맨틱 검색 구성하기

시맨틱 검색을 구성하는 방법에는 두 가지가 있어요:

  • 자동화 워크플로 (빠른 설정에 권장): 최소한의 구성으로 ingest 파이프라인과 인덱스를 자동으로 생성해요.
  • 수동 설정 (커스텀 구성에 권장): 더 큰 유연성과 제어력을 위해 각 구성 요소를 수동으로 구성해요.
  • 시맨틱 필드 사용 (선택적 커스터마이징이 가능한 빠른 설정에 권장): semantic 필드를 사용해 인덱스를 수동으로 구성하면 설정 과정을 단순화하면서도 어느 정도의 구성은 여전히 가능해요.

자동화 워크플로

OpenSearch는 ingest 파이프라인과 인덱스를 모두 자동으로 생성하는 워크플로 템플릿을 제공해요. 워크플로를 생성할 때는 구성한 모델의 모델 ID를 반드시 제공해야 해요. 시맨틱 검색 워크플로 템플릿의 기본값을 검토해서 파라미터를 업데이트해야 하는지 확인해 보세요. 예를 들어 모델의 차원이 기본값(1024)과 다르다면 output_dimension 파라미터에 모델의 차원을 지정하세요. 기본 시맨틱 검색 워크플로를 생성하려면 다음 요청을 보내세요:

POST /_plugins/_flow_framework/workflow?use_case=semantic_search&provision=true
{
  "create_ingest_pipeline.model_id": "mBGzipQB2gmRjlv_dOoB"
}

{% include copy-curl.html %}

OpenSearch는 생성된 워크플로의 워크플로 ID를 응답으로 돌려줘요:

{
  "workflow_id" : "U_nMXJUBq_4FYQzMOS4B"
}

워크플로 상태를 확인하려면 다음 요청을 보내세요:

GET /_plugins/_flow_framework/workflow/U_nMXJUBq_4FYQzMOS4B/_status

{% include copy-curl.html %}

워크플로가 완료되면 state가 COMPLETED로 바뀌어요. 이 워크플로는 다음 구성 요소들을 생성해요:

  • nlp-ingest-pipeline이라는 이름의 ingest 파이프라인
  • my-nlp-index라는 이름의 인덱스

이제 3단계와 4단계를 이어서 진행해 인덱스에 문서를 수집하고 인덱스를 검색할 수 있어요.

수동 설정

시맨틱 검색을 수동으로 구성하려면 다음 단계를 따르세요:

  1. ingest 파이프라인 생성하기.
  2. 수집용 인덱스 생성하기.
  3. 인덱스에 문서 수집하기.
  4. 인덱스 검색하기.

1단계: ingest 파이프라인 생성하기

벡터 임베딩을 생성하려면 문서 필드의 텍스트를 벡터 임베딩으로 변환하는 text_embedding 프로세서이 포함된 ingest 파이프라인을 생성해야 해요. 프로세서의 field_map은 벡터 임베딩을 생성할 입력 필드와 임베딩을 저장할 출력 필드를 결정해요.

다음 예제 요청은 passage_text의 텍스트를 텍스트 임베딩으로 변환하고, 그 임베딩을 passage_embedding에 저장하는 ingest 파이프라인을 생성해요:

PUT /_ingest/pipeline/nlp-ingest-pipeline
{
  "description": "A text embedding pipeline",
  "processors": [
    {
      "text_embedding": {
        "model_id": "bQ1J8ooBpBj3wT4HVUsb",
        "field_map": {
          "passage_text": "passage_embedding"
        }
      }
    }
  ]
}

{% include copy-curl.html %}

긴 텍스트를 여러 passage로 나누려면 text_embedding 프로세서 앞에서 text_chunking ingest 프로세서를 사용하세요. 자세한 내용은 텍스트 청킹을 참고하세요.

2단계: 수집용 인덱스 생성하기

파이프라인에 정의한 텍스트 임베딩 프로세서를 사용하려면, 이전 단계에서 만든 파이프라인을 기본 파이프라인으로 추가해서 벡터 인덱스를 생성하세요. field_map에 정의된 필드가 올바른 타입으로 매핑되어야 해요. 예제를 계속 이어가면, passage_embedding 필드는 모델 차원과 일치하는 차원을 가진 k-NN 벡터로 매핑해야 하고, 마찬가지로 passage_text 필드는 text로 매핑해야 해요.

다음 예제 요청은 기본 ingest 파이프라인이 설정된 벡터 인덱스를 생성해요:

PUT /my-nlp-index
{
  "settings": {
    "index.knn": true,
    "default_pipeline": "nlp-ingest-pipeline"
  },
  "mappings": {
    "properties": {
      "id": {
        "type": "text"
      },
      "passage_embedding": {
        "type": "knn_vector",
        "dimension": 768,
        "method": {
          "engine": "lucene",
          "space_type": "l2",
          "name": "hnsw",
          "parameters": {}
        }
      },
      "passage_text": {
        "type": "text"
      }
    }
  }
}

{% include copy-curl.html %}

벡터 인덱스 생성 방법과 지원되는 메서드에 대한 자세한 내용은 벡터 인덱스 생성하기를 참고하세요.

3단계: 인덱스에 문서 수집하기

이전 단계에서 만든 인덱스에 문서를 수집하려면 다음 요청들을 보내세요:

PUT /my-nlp-index/_doc/1
{
  "passage_text": "Hello world",
  "id": "s1"
}

{% include copy-curl.html %}

PUT /my-nlp-index/_doc/2
{
  "passage_text": "Hi planet",
  "id": "s2"
}

{% include copy-curl.html %}

문서가 인덱스에 수집되기 전에 ingest 파이프라인이 문서에 text_embedding 프로세서를 실행해서 passage_text 필드의 텍스트 임베딩을 생성해요. 색인된 문서에는 원본 텍스트가 담긴 passage_text 필드와 벡터 임베딩이 담긴 passage_embedding 필드가 포함돼요.

4단계: 인덱스 검색하기

인덱스에서 벡터 검색을 수행하려면 Search for a Model API나 Query DSL 쿼리에서 neural 쿼리 절을 사용하세요. 벡터 검색 필터를 사용하면 결과를 더 세분화할 수 있어요.

다음 예제 요청은 Boolean 쿼리를 사용해 필터 절과 두 개의 쿼리 절---neural 쿼리와 match 쿼리를 결합해요. script_score 쿼리는 쿼리 절에 커스텀 가중치를 부여해요:

GET /my-nlp-index/_search
{
  "_source": {
    "excludes": [
      "passage_embedding"
    ]
  },
  "query": {
    "bool": {
      "filter": {
         "wildcard":  { "id": "*1" }
      },
      "should": [
        {
          "script_score": {
            "query": {
              "neural": {
                "passage_embedding": {
                  "query_text": "Hi world",
                  "model_id": "bQ1J8ooBpBj3wT4HVUsb",
                  "k": 100
                }
              }
            },
            "script": {
              "source": "_score * 1.5"
            }
          }
        },
        {
          "script_score": {
            "query": {
              "match": {
                "passage_text": "Hi world"
              }
            },
            "script": {
              "source": "_score * 1.7"
            }
          }
        }
      ]
    }
  }
}

{% include copy-curl.html %}

응답에는 일치하는 문서가 포함돼요:

{
  "took" : 36,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.2251667,
    "hits" : [
      {
        "_index" : "my-nlp-index",
        "_id" : "1",
        "_score" : 1.2251667,
        "_source" : {
          "passage_text" : "Hello world",
          "id" : "s1"
        }
      }
    ]
  }
}

인덱스나 필드에 기본 모델 설정하기

neural 쿼리는 벡터 임베딩을 생성하는 데 모델 ID가 필요해요. neural 쿼리 요청을 보낼 때마다 모델 ID를 전달하는 번거로움을 없애려면 벡터 인덱스나 필드에 기본 모델을 설정할 수 있어요.

먼저 neural_query_enricher 요청 프로세서가 포함된 검색 파이프라인을 생성하세요. 인덱스에 기본 모델을 설정하려면 default_model_id 파라미터에 모델 ID를 제공하세요. 특정 필드에 기본 모델을 설정하려면 neural_field_default_id 맵에 필드 이름과 해당 모델 ID를 제공하세요. default_model_id와 neural_field_default_id를 모두 제공하면 neural_field_default_id가 우선해요:

PUT /_search/pipeline/default_model_pipeline 
{
  "request_processors": [
    {
      "neural_query_enricher" : {
        "default_model_id": "bQ1J8ooBpBj3wT4HVUsb",
        "neural_field_default_id": {
           "my_field_1": "uZj0qYoBMtvQlfhaYeud",
           "my_field_2": "upj0qYoBMtvQlfhaZOuM"
        }
      }
    }
  ]
}

{% include copy-curl.html %}

그다음 인덱스에 기본 모델을 설정하세요:

PUT /my-nlp-index/_settings
{
  "index.search.default_pipeline" : "default_model_pipeline"
}

{% include copy-curl.html %}

이제 검색할 때 모델 ID를 생략할 수 있어요:

GET /my-nlp-index/_search
{
  "_source": {
    "excludes": [
      "passage_embedding"
    ]
  },
  "query": {
    "neural": {
      "passage_embedding": {
        "query_text": "Hi world",
        "k": 100
      }
    }
  }
}

{% include copy-curl.html %}

응답에는 두 문서가 모두 포함돼요:

{
  "took" : 41,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.22762,
    "hits" : [
      {
        "_index" : "my-nlp-index",
        "_id" : "2",
        "_score" : 1.22762,
        "_source" : {
          "passage_text" : "Hi planet",
          "id" : "s2"
        }
      },
      {
        "_index" : "my-nlp-index",
        "_id" : "1",
        "_score" : 1.2251667,
        "_source" : {
          "passage_text" : "Hello world",
          "id" : "s1"
        }
      }
    ]
  }
}

시맨틱 필드 사용하기

semantic 필드를 사용해 시맨틱 검색을 수동으로 구성하려면 다음 단계를 따르세요. semantic 필드 사용 시 제약 사항을 포함한 자세한 내용은 시맨틱 필드 타입을 참고하세요.

1단계: 시맨틱 필드가 있는 인덱스 생성하기

인덱스를 생성하고 semantic 필드에 model_id를 지정하세요. 이 예제에서 semantic 필드는 passage_text예요. OpenSearch는 모델 구성을 기반으로 대응하는 임베딩 필드를 자동으로 생성해요. ingest 파이프라인은 필요 없어요---OpenSearch가 색인하는 동안 지정된 모델을 사용해 임베딩을 자동으로 생성해요:

PUT /my-nlp-index
{
  "settings": {
    "index.knn": true
  },
  "mappings": {
    "properties": {
      "id": {
        "type": "text"
      },
      "passage_text": {
        "type": "semantic",
        "model_id": "9kPWYJcBmp4cG9LrbAvW"
      }
    }
  }
}

{% include copy-curl.html %}

인덱스를 만든 후에는 매핑을 조회해서 임베딩 필드가 자동으로 생성되었는지 확인할 수 있어요:

GET /my-nlp-index/_mapping
{
  "my-nlp-index": {
    "mappings": {
      "properties": {
        "id": {
          "type": "text"
        },
        "passage_text": {
          "type": "semantic",
          "model_id": "9kPWYJcBmp4cG9LrbAvW",
          "raw_field_type": "text"
        },
        "passage_text_semantic_info": {
          "properties": {
            "embedding": {
              "type": "knn_vector",
              "dimension": 384,
              "method": {
                "engine": "faiss",
                "space_type": "l2",
                "name": "hnsw",
                "parameters": {}
              }
            },
            "model": {
              "properties": {
                "id": {
                  "type": "text",
                  "index": false
                },
                "name": {
                  "type": "text",
                  "index": false
                },
                "type": {
                  "type": "text",
                  "index": false
                }
              }
            }
          }
        }
      }
    }
  }
}

{% include copy-curl.html %}

2단계: 인덱스에 문서 수집하기

이전 단계에서 만든 인덱스에 문서를 수집하려면 다음 요청을 보내세요:

PUT /my-nlp-index/_doc/1
{
  "passage_text": "Hello world",
  "id": "s1"
}

{% include copy-curl.html %}

문서가 인덱스에 수집되기 전에 OpenSearch가 내장 ingest 파이프라인을 실행해서 임베딩을 생성하고 passage_text_semantic_info.embedding 필드에 저장해요. 임베딩이 제대로 생성되었는지 확인하려면 검색 요청을 실행해 문서를 조회할 수 있어요:

GET /my-nlp-index/_doc/1
{
  "_index": "my-nlp-index",
  "_id": "1",
  "_version": 1,
  "_seq_no": 0,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "passage_text": "Hello world",
    "passage_text_semantic_info": {
      "model": {
        "name": "huggingface/sentence-transformers/all-MiniLM-L6-v2",
        "id": "9kPWYJcBmp4cG9LrbAvW",
        "type": "TEXT_EMBEDDING"
      },
      "embedding": [
        -0.034477286,
        ...
      ]
    },
    "id": "s1"
  }
}

{% include copy-curl.html %}

3단계: 인덱스 검색하기

semantic 필드의 임베딩을 쿼리하려면 semantic 필드의 이름(이 예제에서는 passage_text)과 쿼리 텍스트를 제공하세요. model_id를 지정할 필요는 없어요---OpenSearch가 인덱스 매핑의 필드 구성에서 자동으로 이를 가져와서, 기반이 되는 임베딩 필드를 대상으로 하도록 쿼리를 재작성해요:

GET /my-nlp-index/_search
{
  "_source": {
    "excludes": [
      "passage_text_semantic_info"
    ]
  },
  "query": {
    "neural": {
      "passage_text": {
        "query_text": "Hi world"
      }
    }
  }
}

{% include copy-curl.html %}

응답에는 일치하는 문서가 포함돼요:

{
  "took": 48,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.7564365,
    "hits": [
      {
        "_index": "my-nlp-index",
        "_id": "1",
        "_score": 0.7564365,
        "_source": {
          "passage_text": "Hello world",
          "id": "s1"
        }
      }
    ]
  }
}

다음 단계

더 알아보기 (Learn more)