-
Go: 카카오맵 API 지하철 역 주변 검색하기 (blevesearch)컴퓨터/Go language 2024. 10. 20. 20:21728x90반응형
현재 대한민국 철봉 지도 (https://www.k-pullup.com)
검색 기능에 "수원역"처럼 역 이름으로 검색하면 검색이 안 되는 문제가 있다.
인덱싱 할 때 주소를 ngram 방식으로 검색하면 쓸모없는 위치가 많이 검색된다.
또한 "서울대입구역" 처럼 "서울대입구"는 대한민국 주소상 존재하지 않을 수 있다.
따라서 "서울대입구역"을 검색하면 그 주변 (ex 3km 반경)에 있는 나의 데이터들을 보여주고 싶었다.
1. 데이터
서울시 역사마스터 정보를 보면 (최신으로 잘 업데이트되는 느낌)
750개가 넘는 도시철도 역들의 좌표가 잘 나와있다. (지하철 + 고가철도 etc) (WGS84 좌표계)
전국 5대 지하철을 받으려면 위 데이터에 직접 대전/대구/광주/부산을 추가할 수 밖에 없다.
이걸 아래처럼 파싱해서 앱 메모리에 넣었다. (더 나은 데이터 저장법 찾기)
type KoreaStation struct { Name string Latitude float64 Longitude float64 } func NewStationData() (map[string]dto.KoreaStation, error) { stationMap := make(map[string]dto.KoreaStation) file, err := os.Open("./stations.json") if err != nil { return nil, err } defer file.Close() decoder := sonic.ConfigDefault.NewDecoder(file) // json var data struct { Data []struct { BldnNm string `json:"bldn_nm"` Lat string `json:"lat"` Lot string `json:"lot"` } `json:"DATA"` } if err := decoder.Decode(&data); err != nil { return nil, err } for _, item := range data.Data { lat, err := strconv.ParseFloat(item.Lat, 64) if err != nil { continue } lon, err := strconv.ParseFloat(item.Lot, 64) if err != nil { continue } name := item.BldnNm // 서울대입구(관악구청) 같은 것을 서울대입구로 if idx := strings.Index(name, "("); idx != -1 { name = name[:idx] } // "역"으로 끝내기 if !strings.HasSuffix(name, "역") { name = name + "역" } stationMap[name] = dto.KoreaStation{ Name: name, Latitude: lat, Longitude: lon, } } return stationMap, nil }
2. 검색
https://github.com/blevesearch/bleve/issues/2080
blevesearch가 문서가 상당히 빈약하다.
이슈를 통해 찾아보니 아래처럼 좌표를 인덱싱하여 사용할 수 있다.
(꼭 DefaultMapping을 지정해 줘야 좌표가 일반 숫자로 변환되는 것을 막을 수 있다고 한다)
package main import ( "fmt" "github.com/blevesearch/bleve/v2" "github.com/blevesearch/bleve/v2/search" ) type data struct { Name string `json:"name"` Location Location `json:"location"` Tags map[string]string `json:"tags"` Type string `json:"type"` } type Location struct { Lat float64 Lon float64 } func main() { // open a new index indexMapping := bleve.NewIndexMapping() newMapping := bleve.NewDocumentMapping() newMapping.AddFieldMappingsAt("name", bleve.NewTextFieldMapping()) locationMapping := bleve.NewGeoPointFieldMapping() locationMapping.IncludeTermVectors = true locationMapping.IncludeInAll = true locationMapping.Index = true locationMapping.Store = true locationMapping.Type = "geopoint" newMapping.AddFieldMappingsAt("location", locationMapping) tagsMapping := bleve.NewKeywordFieldMapping() tagsMapping.Analyzer = "" tagsMapping.Index = false tagsMapping.Store = true newMapping.AddFieldMappingsAt("tags", tagsMapping) indexMapping.AddDocumentMapping("myDoc", newMapping) indexMapping.DefaultMapping = newMapping indexMapping.TypeField = "type" index, err := bleve.NewMemOnly(indexMapping) //index, err := bleve.NewUsing("_index.bleve", indexMapping, scorch.Name, upsidedown.Name, nil) if err != nil { panic(err) } letsIndexDoc(index) doLoctionSearch(index) //os.RemoveAll("_index.bleve") } func doLoctionSearch(idx bleve.Index) { long := 77.684164354519 // Same lat long as the first doc ABC1 lat := 13.019409351686077 //distance query distanceQuery := bleve.NewGeoDistanceQuery(long, lat, "10km") distanceQuery.SetField("location") searchRequest := bleve.NewSearchRequest(distanceQuery) searchRequest.Fields = []string{"location", "name", "tags"} searchResults, err := idx.Search(searchRequest) fmt.Println("ERR: ", err) fmt.Println("searchResult1 -> ", searchResults) for _, hit := range searchResults.Hits { var name = hit.Fields["name"].(string) var tags = hit.Fields["Tags"].(map[string]string) fmt.Printf("[%s] %s %+v\n", hit.ID, name, tags) } // Sorted order of sports complex query termQuery := bleve.NewMatchAllQuery() searchRequest2 := bleve.NewSearchRequestOptions(termQuery, 10, 0, false) sortGeo, _ := search.NewSortGeoDistance("location", "km", long, lat, true) searchRequest2.SortByCustom(search.SortOrder{sortGeo}) searchResult2, err := idx.Search(searchRequest2) fmt.Println("ERR: ", err) fmt.Println("searchResult2 -> ", searchResult2) } func letsIndexDoc(idx bleve.Index) { d := data{ Name: "ABC1", Location: Location{ Lat: 13.019409351686077, Lon: 77.684164354519, }, Tags: map[string]string{"tag1": "a", "tag2": "b"}, Type: "myDoc", } idx.Index("ABC1", d) d = data{ Name: "ABC2", Location: Location{ Lat: 13.020717704600788, Lon: 77.68196396638403, }, Tags: map[string]string{"tag1": "A", "tag3": "B"}, Type: "myDoc", } idx.Index("ABC2", d) }
위처럼 데이터에 맞게 인덱싱 후 검색 query는 아래처럼 날릴 수 있다.
func (s *BleveSearchService) SearchMarkersNearLocation(t string) (dto.MarkerSearchResponse, error) { // 여기서 위에서 만든 stationMap에서 이름을 확인한다 // Trie 써서 prefix search 해서 해도 될 듯하다. var lat, lon float64 if station, ok := s.stationMap[t]; ok { lat = station.Latitude lon = station.Longitude s.Logger.Info("Station name matches", zap.String("station", t), zap.Float64("lat", lat), zap.Float64("lon", lon)) } response := dto.MarkerSearchResponse{Markers: make([]dto.ZincMarker, 0)} // 확인 결과 "3.5km" 와 같이 float도 됨, Query에는 longitude/latitude 순 // 인덱싱 때에만 latitude/longitude 순서다. distance := "5km" geoQuery := bleve.NewGeoDistanceQuery(lon, lat, distance) geoQuery.SetField("coordinates") // 필자는 coordinates 이름을 사용했음 searchRequest := bleve.NewSearchRequestOptions(geoQuery, 15, 0, false) searchRequest.Fields = []string{"fullAddress", "coordinates"} searchRequest.SortBy([]string{"_score", "markerId"}) searchResult, err := s.Index.Search(searchRequest) if err != nil { return response, err } response.Took = int(searchResult.Took.Milliseconds()) response.Markers = extractMarkers(searchResult.Hits) return response, nil }
Spring Boot에서 이용할 땐 아래 코드를 참고하면 좋다.
https://github.com/Alfex4936/K-Festia/commit/2053b3d68cd83b5667f7306cc2ee78b93d2fae76
728x90'컴퓨터 > Go language' 카테고리의 다른 글
Blurhash: 이미지 미리보기 블러 라이브러리 placeholder (2) 2024.12.14 Go: Fiber 서버 최적화 하기 (optimization) (0) 2024.09.12 Go: Bleve 인덱싱 한국어 주소 검색 서버 만들기 (Apache Lucene-like) (0) 2024.06.02