ABOUT ME

-

Total
-
  • Go: 카카오맵 API 지하철 역 주변 검색하기 (blevesearch)
    컴퓨터/Go language 2024. 10. 20. 20:21
    728x90
    반응형

    역 주변 검색 기능

    현재 대한민국 철봉 지도 (https://www.k-pullup.com)

    검색 기능에 "수원역"처럼 역 이름으로 검색하면 검색이 안 되는 문제가 있다.

    인덱싱 할 때 주소를 ngram 방식으로 검색하면 쓸모없는 위치가 많이 검색된다.

    또한 "서울대입구역" 처럼 "서울대입구"는 대한민국 주소상 존재하지 않을 수 있다.

     

    따라서 "서울대입구역"을 검색하면 그 주변 (ex 3km 반경)에 있는 나의 데이터들을 보여주고 싶었다.

     

    1. 데이터

     

    열린데이터광장 메인

    데이터분류,데이터검색,데이터활용

    data.seoul.go.kr

    서울시 역사마스터 정보를 보면 (최신으로 잘 업데이트되는 느낌)

    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

     

    Struggling with making NewGeoDistanceQuery work · Issue #2080 · blevesearch/bleve

    Hello, I have this simple code: https://git.sr.ht/~cgenie/bleve-geo-test/tree/master/item/main.go I basically copied it from issue #1789 and applied the fixes about indexMapping.TypeField = "type"....

    github.com

     

    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

    댓글