'ITWeb/검색일반'에 해당되는 글 28건

  1. 2017.06.23 [Arirang] 사전 기반으로만 형태소 분석 처리 해보기
  2. 2017.06.09 [Arirang Analyzer - lucene 6.5.0] Term startOffset 정렬 오류
  3. 2017.06.09 [Arirang] first position increment must be > 0 오류
  4. 2017.05.31 [검색] SEO 태그 가이드
  5. 2017.01.19 [Lucene] Multi-value fields and the inverted index
  6. 2016.10.21 [OCR] 광학문자인식 정보
  7. 2016.10.12 [우편번호검색] 우편번호검색 서비스를 만들기 위한 기본 정보
  8. 2016.06.27 [Javascript] English to Korean (영문 한글 전환)
  9. 2016.04.27 [검색이론] Recall 과 Precision - wikipedia
  10. 2016.03.30 [Lucene] TermVector 정보 중 Offset 에 대해서.

[Arirang] 사전 기반으로만 형태소 분석 처리 해보기

ITWeb/검색일반 2017. 6. 23. 13:51

그냥 사전만 가지고 몇 가지 형태소 분석 처리를 하기 위한 팁 정보 입니다.

한마디로 노가다 입니다.

모든 부분에 공통적으로 적용 되는 것은 아니며 사용 형태에 따라 수정 하셔야 하는 부분이니 그냥 참고 정도만 하자라고 생각해 주세요.


공통)

- 복합명사 분해 시 분해 된 단어가 용언일 경우 복합명사를 사용하지 말고 확장사전에 등록해서 사용을 합니다.

  또는 분해 된 단어가 용언일 경우 찾아서 체언 처리를 해줍니다.

- 체언과 기타품사 차이는 체언은 단독으로 사용 시 형태소 분석이 되지만 기타품사는 분석 되지 않습니다.


  복합명사)

    그리는게:그리는,게:0000


  확장사전)

    그리,100000000X

    그리는게,100000000X


  분해)

    그리는게

      그리는게

    그리는

      그리


'그리는' 자체를 체언으로 분해 하고 싶을 경우 확장 사전에 체언으로 등록이 되어야 하며, 그리에 대한 용언도 동일하게 체언처리가 되어야 합니다.



- '~요', '~해요' 로 끝나는 용언 처리

  좋아요

  좋아해요


  확장사전)

    좋아,100000000X

    좋아해,100000000X



- '~져', '~져서', '~서' 로 끝나는 용언 처리

  기존 용언으로 등록된 단어를 체언으로 변경 해야 합니다.

  010000000X -> 100000000X

  '~서' 의 경우 사전에 '서,110000000X' 와 같이 등록이 되어 있어 복합명사 사전에 추가 등록을 합니다.

  복합명사 등록 시 분해된 명사에 대한 확장사전 등록이 되어 있어야 합니다.


  확장사전)

    어두워지,100000000X

    어두워,100000000X

    늘어지,100000000X

    늘어져,100000000X


  복합명사)

    어두워서:어두워서,어두워:0000

    어두워져:어두워져,어두워:0000

    어두워져서:어두워져서,어두워:0000

    늘어져서:늘어져서,늘어져:0000



- 복합용언 + '~요' 로 끝나는 용언 처리

  크고낮아요

  말려들어요


  복합명사)

    크고낮아:크고,낮아:0000

    말려들어:말려,들어:0000



- '~다', '~데' 로 끝나는 용언 처리

  크다

  작다

  큰데

  작은데

  '~다' 끝나는 용언이 형태소분리가 되기 위해서는 확장사전에 등록이 되어야 합니다.


  확장사전)

    크다,100000000X

    작다,100000000X

    큰데,100000000X

    작은데,100000000X

  


- '~ㄴ', '~은', '~는' 으로 끝나는 용언 처리

  짧은

  넒은

  튀어나온

  어울리는

    어울리,010000000X 용언 처리가 되어 있기 때문에 체언으로 fully 등록 합니다.

  잃어가는


  확장사전)

    짧은,100000000X

    넓은,100000000X

    튀어나온,100000000X

    잃어가는,100000000X

    어울리는,100000000X



- 'ㅎ' 불규칙 용언 처리

  노랗고

  동그랗고


  확장사전)

    노랗,100000000X


  복합명사)

    노랗고:노랗고,노랗:0000


- '~하', '~한' 으로 끝나는 용언 처리

  확장사전에 용언 처리가 되어 있는지 확인 합니다.

  용언 처리가 되어 있다면 체언으로 변경해 줍니다.

  확장사전에 ~하, ~한 을 제외 및 하다 동사 표기를 포함한 체언으로 등록 합니다.


  확장사전 1)

    넓적하,010000000X -> 100000000X

    넓적,100100000X

:

[Arirang Analyzer - lucene 6.5.0] Term startOffset 정렬 오류

ITWeb/검색일반 2017. 6. 9. 10:23

[arirang-analyzer-6.5.0]

  term analyzed 시 startOffset 정보에 대한 정렬이 역전 되는 오류

  개별 term 에서의 startOffset 이 역전 되기 때문에 아래 class 의 method 에서 정렬을 다시 맞춰줍니다.

  (정상적인 방법 이라기 보다는 일단 문제를 회피하기 위한 방법 입니다.)


  Class : KoreanFilter

  Method 1 :

    private void analysisKorean(String input) throws MorphException {


  //  input = trimHangul(input);

      List<AnalysisOutput> outputs = morph.analyze(input);

      if (outputs.size() == 0) {

        return;

      }


      Map<String, KoreanToken> map = new LinkedHashMap<String, KoreanToken>();

      if (hasOrigin) {

        map.put("0:" + input, new KoreanToken(input, offsetAtt.startOffset()));

      }


      extractKeyword(outputs, offsetAtt.startOffset(), map, 0);


      Collection<KoreanToken> values = map.values();

      for (KoreanToken kt : values) {

        kt.setOutputs(outputs);

      }


      // 이 부분에서 map 에 등록된 정보를 정렬 합니다.


      morphQueue.addAll(map.values());

    }


  Method 2 :

    private void analysisKorean(String input) throws MorphException {


  //  input = trimHangul(input);

      List<AnalysisOutput> outputs = morph.analyze(input);

      if (outputs.size() == 0) {

        return;

      }


      Map<String, KoreanToken> map = new LinkedHashMap<String, KoreanToken>();

      if (hasOrigin) {

        map.put("0:" + input, new KoreanToken(input, offsetAtt.startOffset()));

      }


      extractKeyword(outputs, offsetAtt.startOffset(), map, 0);


      Collection<KoreanToken> values = map.values();

      for (KoreanToken kt : values) {

        kt.setOutputs(outputs);

      }


      morphQueue.addAll(map.values());

      // 이 부분에서 morphQueue 에 등록된 정보를 정렬 합니다.

      morphQueue.sort(Comparator.comparingInt(KoreanToken::getOffset));

    }


  Method 3 : 

    protected void extractKeyword(List<AnalysisOutput> outputs, int startoffset,

      final Map<String, KoreanToken> map, int position) {

      ... 원본 코드 생략


      // 이 부분에서 map 에 대한 등록된 정보를 정렬 합니다.

    }


  정렬 방법 :

    참고) https://stackoverflow.com/questions/109383/sort-a-mapkey-value-by-values-java


    final List<Map.Entry<String, KoreanToken>> offsetSorts = map.entrySet().stream()

        .sorted(Map.Entry.comparingByValue(Comparator.comparingInt(KoreanToken::getOffset)))

        .collect(Collectors.toList());


    map.clear();


    offsetSorts.stream().forEachOrdered(e -> map.put(e.getKey(), e.getValue()));


  Method 4 :

    KoreanFilter 를 상속받아 CustomKoreanFilter 를 만들어 사용 하면 됩니다.


:

[Arirang] first position increment must be > 0 오류

ITWeb/검색일반 2017. 6. 9. 10:22

아직 확인 및 테스트 하지 않았습니다. ^^;

그냥 코드만 보고 이렇게 하면 되겠다 정도만 입니다.


- DefaultIndexingChain.java

first position increment must be > 0


관련 에러 수정을 위해서는 KoreanFilter.java 내 posIncrAtt.setPositionIncrement(iw.getPosInc()); 영역에서 

iw.getPosInc() 가 -1 인지 검사해서 1로 변경을 해줍니다.

변경에 따른 오류에 대해서 검토가 필요 합니다.


  private void setAttributesFromQueue(boolean isFirst) {

    final KoreanToken iw = morphQueue.removeFirst();

    if (isFirst && !morphQueue.isEmpty()) {

      // our queue has more elements remaining (e.g. we decompounded)

      // capture state for those. We set the term attribute to be empty

      // so we save lots of array copying later.

      termAtt.setEmpty();

      currentState = captureState();

    }

 

    termAtt.setEmpty().append(iw.getTerm());

    offsetAtt.setOffset(iw.getOffset(), iw.getOffset() + iw.getLength());

    morphAtt.setToken(iw);


    // on the first Token we preserve incoming increment:

    if (!isFirst) {

      posIncrAtt.setPositionIncrement(iw.getPosInc());

    }

    

    String type = TokenUtilities.getType(iw.getTerm().toCharArray(), iw.getTerm().length());

    typeAtt.setType(type);

    

    // TODO: How to handle PositionLengthAttribute correctly?

  }



:

[검색] SEO 태그 가이드

ITWeb/검색일반 2017. 5. 31. 16:52

- 네이버 검색 관련 가이드 : http://webmastertool.naver.com/guide/basic_optimize.naver

- 구글 검색 관련 가이드 : https://developers.google.com/search/docs/guides/search-gallery 

- 페이스북 오픈 그래프 태그 가이드 : https://developers.facebook.com/docs/sharing/webmasters#markup

- 페이스북 앱 링크 태그 가이드 : https://developers.facebook.com/docs/applinks/metadata-reference


:

[Lucene] Multi-value fields and the inverted index

ITWeb/검색일반 2017. 1. 19. 18:41
아주 기초적인 것도 잊어버리는 것 같아 기록해 봅니다.

Multi-value fields and the inverted index

The fact that all field types support multi-value fields out of the box is a consequence of the origins of Lucene. Lucene was designed to be a full text search engine. In order to be able to search for individual words within a big block of text, Lucene tokenizes the text into individual terms, and adds each term to the inverted index separately.

This means that even a simple text field must be able to support multiple values by default. When other datatypes were added, such as numbers and dates, they used the same data structure as strings, and so got multi-values for free.


이 글은 아래 elasticsearch 에서 퍼왔습니다.


[문서]

https://www.elastic.co/guide/en/elasticsearch/reference/2.4/array.html

:

[OCR] 광학문자인식 정보

ITWeb/검색일반 2016. 10. 21. 13:01

조만간 사용을 해야해서 일단 링크 투척


https://github.com/tesseract-ocr/tesseract

https://github.com/jflesch/pyocr



opencv 도 링크 투척


http://docs.opencv.org/2.4.9/modules/refman.html


:

[우편번호검색] 우편번호검색 서비스를 만들기 위한 기본 정보

ITWeb/검색일반 2016. 10. 12. 17:47


우편번호 검색 기능을 구현 하다 보니 우체국 DB로는 daily 변경 데이터 반영이 어려워 그냥 행자부 DB를 가지고 만드는게 좋겠다는 결론을 내렸습니다.


방법은)

1. 우체국 DB + 행자부 변경 DB to 우체국 DB

2. 행자부 DB + 행자부 변경 DB


================================


우편번호 검색 서비스를 만들기 위해서 추려본 내용입니다.

그냥 편하게 행자부나 우체국에서 제공하는 오픈API를 사용해도 되지만 직접 구축해보고 싶어하는 분들을 위해 공유해 봅니다.


[도로명주소 오픈API]

https://www.juso.go.kr/addrlink/devAddrLinkRequestSample.htm


[우체국 오픈API]

http://biz.epost.go.kr/customCenter/custom/custom_10.jsp


[우체국 DB 다운로드]

http://www.epost.go.kr/search/zipcode/cmzcd002k01.jsp


[도로/지번 주소와 우편번호]

  도로/지번 주소는 행정자치부에서 관리

  우편번호는 행자부 데이터를 받아와서 우체국에서 매칭 및 생성 관리 (일부 데이터는 누락 될 수 있음)


[우편번호 PK]

  건물관리번호


[도로명]

  우편번호 / 시도(영문) / 시군구(영문) / 읍면(영문) / 도로명(영문) / 지하여부 /  건물번호본 - 건물번호부 / (법정동명, 리명, 시군구동건물명)

    지하여부 : 1 일 경우 지하 + 건물번호본 - 건물번호부 / (법정동명, 리명, 시군구동건물명) 로 지정 합니다. (지하 31)


[지번]

  우편번호 / 시도(영문) / 시군구(영문) / 읍면(영문) / 법정동명 / 리명 / 산여부 / 지번본번 - 지번부번 / (시군구동건물명)

    산여부 : 1 일 경우 산 + 지번본번 - 지번부번 / (시군구동건물명) 로 지정 합니다. (산109-7)


[사서함]

  우편번호 / 시도 / 시군구 / 읍면 / 사서함명 / 시작사서함주번호 - 시작사서함부번호 / 끝사서함주번호 - 끝사서함부번호

    사서함번호는 시작부터-끝까지 (전체) 로 표기 합니다. (1-2500 (전체))


[도로명 범위]

  우편번호 / 시도(시도영문) / 시군구(시군구영문) / 읍면(영문) / 도로명(영문) / 지하여부 / 시작건물번호(주) - 시작건물번호(부) / 끝건물번호(주) - 끝건물번호(부)

    시작건물번호-끝건물번호 표현

      382, 0 - 384, 1 = 382, 383, 384, 384-1


[지번 범위]

  우편번호 / 시도(영문) / 시군구(영문) / 읍면동(영문) / 리명 / 산여부 / 시작주번지 - 시작부번지 / 끝주번지 - 끝부번지


[인덱스 종류]

  통합주소 DB

  사서함 + 도로명 범위 + 지번 범위 DB


[검색 대상 필드]

  도로명(법정동명 + 리명) + 건물번호 

  읍/면/동/리 + 지번

  건물명 (시군구동건물명)

  사서함 + 사서함번호


[통합검색]

  시도 / 시군구 / 읍면 / 도로명 / 법정동명 / 리명 / 건물번호본 / 건물번호부 / 지번본번 / 지번부번 / 시군구동건물명


[화면출력]

  우편번호 / 시도(영문) / 시군구(영문) / 읍면(영문) / 도로명(영문) / 지하여부 /  건물번호본 - 건물번호부 / (법정동명, 리명, 시군구동건물명)

  우편번호 / 시도(영문) / 시군구(영문) / 읍면(영문) / 법정동명 / 리명 / 산여부 / 지번본번 - 지번부번 / (시군구동건물명)

  우편번호 / 시도 / 시군구 / 읍면 / 사서함명 / 시작사서함주번호 - 시작사서함부번호 / 끝사서함주번호 - 끝사서함부번호

  우편번호 / 시도(시도영문) / 시군구(시군구영문) / 읍면(영문) / 도로명(영문) / 지하여부 / 시작건물번호(주) - 시작건물번호(부) / 끝건물번호(주) - 끝건물번호(부)

  우편번호 / 시도(영문) / 시군구(영문) / 읍면동(영문) / 리명 / 산여부 / 시작주번지 - 시작부번지 / 끝주번지 - 끝부번지


[정렬]

  시도 / 시군구 / 우편번호 / score


[필터]

  시도 / 시군구


위 내용에서 입맛에 맞게 고치시면 됩니다.

꼭 저렇게 해야 한다는 것이 절대 아닙니다.

요구사항에 맞게 수정해서 만드시면 되겠습니다. :) 

:

[Javascript] English to Korean (영문 한글 전환)

ITWeb/검색일반 2016. 6. 27. 23:51

필요해서 구글링으로 퍼왔습니다.

기본적으로는 한글 자모 분리 기능 구현을 사용한다고 보시면 됩니다.


아래 코드 중 구글링으로 퍼온 코드에서 읽기 쉽도록 약간의 변수명등 수정을 했습니다.

자바스크립트에서 function 선언에 대한 기본 이해를 하시면 코드 보기가 더 쉽습니다.


<html>

<head></head>

<script>

var convertEngToKor = function(args) {

var engChosung = "rRseEfaqQtTdwWczxvg"

var engChosungReg = "[" + engChosung + "]";

var engJungsung = {k:0,o:1,i:2,O:3,j:4,p:5,u:6,P:7,h:8,hk:9,ho:10,hl:11,y:12,n:13,nj:14,np:15,nl:16,b:17,m:18,ml:19,l:20};

var engJungsungReg = "hk|ho|hl|nj|np|nl|ml|k|o|i|O|j|p|u|P|h|y|n|b|m|l";

var engJongsung = {"":0,r:1,R:2,rt:3,s:4,sw:5,sg:6,e:7,f:8,fr:9,fa:10,fq:11,ft:12,fx:13,fv:14,fg:15,a:16,q:17,qt:18,t:19,T:20,d:21,w:22,c:23,z:24,x:25,v:26,g:27};

var engJongsungReg = "rt|sw|sg|fr|fa|fq|ft|fx|fv|fg|qt|r|R|s|e|f|a|q|t|T|d|w|c|z|x|v|g|";

var regExp = new RegExp("("+engChosungReg+")("+engJungsungReg+")(("+engJongsungReg+")(?=("+engChosungReg+")("+engJungsungReg+"))|("+engJongsungReg+"))","g");


var converter = function (args, cho, jung, jong) {

return String.fromCharCode(engChosung.indexOf(cho) * 588 + engJungsung[jung] * 28 + engJongsung[jong] + 44032);

};

var result = args.replace(regExp, converter);

console.log(result);

return result;

}


function run(engStr) {

convertEngToKor(engStr);

}


var convertEngToKor2 = (function () {

var engChosung = "rRseEfaqQtTdwWczxvg"

var engChosungReg = "[" + engChosung + "]";

var engJungsung = {k:0,o:1,i:2,O:3,j:4,p:5,u:6,P:7,h:8,hk:9,ho:10,hl:11,y:12,n:13,nj:14,np:15,nl:16,b:17,m:18,ml:19,l:20};

var engJungsungReg = "hk|ho|hl|nj|np|nl|ml|k|o|i|O|j|p|u|P|h|y|n|b|m|l";

var engJongsung = {"":0,r:1,R:2,rt:3,s:4,sw:5,sg:6,e:7,f:8,fr:9,fa:10,fq:11,ft:12,fx:13,fv:14,fg:15,a:16,q:17,qt:18,t:19,T:20,d:21,w:22,c:23,z:24,x:25,v:26,g:27};

var engJongsungReg = "rt|sw|sg|fr|fa|fq|ft|fx|fv|fg|qt|r|R|s|e|f|a|q|t|T|d|w|c|z|x|v|g|";

var regExp = new RegExp("("+engChosungReg+")("+engJungsungReg+")(("+engJongsungReg+")(?=("+engChosungReg+")("+engJungsungReg+"))|("+engJongsungReg+"))","g");


var converter = function (args, cho, jung, jong) {

return String.fromCharCode(engChosung.indexOf(cho) * 588 + engJungsung[jung] * 28 + engJongsung[jong] + 44032);

};


return (function (args) {

var result = args.replace(regExp, converter); 

console.log(result);

return result; 

});

})();


function run2(engStr) {

convertEngToKor2(engStr);

}

</script>

<body>

<input id="eng" value="skdlzl">

<button onclick="run(document.getElementById('eng').value)">run</button>

<button onclick="run2(document.getElementById('eng').value)">run2</button>

</body>

</html>


코드를 보시면 아시겠지만, 영문으로 작성한 skdlrl(나이키) 를 한글 나이키로 변환해서 리턴해 주도록 해줍니다.

보통 검색에서 자동완성 기능 구현 시 client side 에서 한영변환에 대한 기능으로 활용하기 위해 사용 합니다.


:

[검색이론] Recall 과 Precision - wikipedia

ITWeb/검색일반 2016. 4. 27. 11:15

그냥 복습 차원에서 위키피디아에 있는 내용을 그대로 작성해 본 것입니다.


기본 IR 이론)

recall = Number of relevant documents retrieved / Total number of relevant documents

precision = Number of relevant documents retrieved / Total number of documents retrieved


원문링크)

https://ko.wikipedia.org/wiki/%EC%A0%95%EB%B0%80%EB%8F%84%EC%99%80_%EC%9E%AC%ED%98%84%EC%9C%A8



Precision, 정밀도 라고 되어 있는데 저는 그냥 정확도 라고 부릅니다.

이유는 뭐 별거 없고 말이 이게 더 쉽게 전달 되는것 같아서 이구요.

얼마나 관련(relevant) 있는 문서들이 나왔는지를 보는 지표 라서 그렇게 부릅니다.


Precision =  | {relevant documents} ∩ {retrieved documents} | / | {retrieved documents} |

정확도 = (관련문서 수  ∩  검색된 문서 수) / 검색된 문서 수


Recall, 이건 재현율 이라고 부릅니다.

precision 과는 약간 상충 되는 내용이기도 합니다.

그래서 둘 다 높히기는 참 어려운 것 같습니다.

이것은 관련(relevant) 있는 문서들 중 실제로 검색된 문서들의 비율이 됩니다.


Recall = | {relevant documents} ∩ {retrieved documents} | / | {relevant documents} |


두 개의 차이는 분모 부분이 다르다는 것입니다.

이 정보들은 실제 통계학에서도 동일하게 사용 됩니다.


False Positive/Negative

True Positive/Negative


음... 사실 저는 과거에 스팸 필터 엔진 만들때 사용하던 내용이였는데요.

스팸으로 표현 하면 스팸 문서가 아닌데 스팸 문서라고 하는게 false positive, 서버 장애가 아닌데 장애라고 하는 것도 같은 의미 입니다. 이런건 false alarm 이라고도 합니다.


false negative 는 false positive 와 반대겠죠.

스팸 문서 인데 스팸 문서가 아니라고 하는 것입니다. 실제 서버는 장애가 났는데 장애 알람이 오지 않은 경우가 되겠습니다.


그럼 true positive는 무엇일까요? 이건 그냥 정상 입니다.

스팸 문서를 스팸 문서라고 하는 것이구요. true negative 는 그렇습니다. 스팸 문서가 아닌걸 스팸문서가 아니라고 하는 것이 되겠습니다.


검색으로 풀면 )

 

 관련된 문서를

 관련 안된 문서를

 관련된 문서라고 함

 True Positive (TP)

 False Positive (FP)

 관련 안된 문서라고 함

 False Negative (TN)

 True Negative (TN)


통계적 관점에서의 계산 식은 아래와 같습니다.


Precision(Positive predictive value:PPV) = TP / (TP + FP)


Recall(Sensitivity) = TP / (TP + FN)


True Negative Rate(Specificity) = TN / (TN + FP)


Accuracy = (TP + TN) / (TP + TN + FP + FN)


여기까지 복습 차원에서 정리해 봤습니다.


:

[Lucene] TermVector 정보 중 Offset 에 대해서.

ITWeb/검색일반 2016. 3. 30. 17:33

아는 것도 이제는 기억이 가물가물 합니다. 그래서 또 기록해 봅니다.

사내 교육을 하면서 lucene 기본 이론 교육을 하다, start offset 과 end offset 에 대해서 설명을 해주고 있었는데요.

end offset 이 실제 text의 offset 값 보다 1 크다는 것에 대한 질문이 있었습니다.


아는 건데 일단 가볍게라도 설명하고 넘어 가야해서 아무래도 highlight 기능을 위해서 그렇게 설정 하는것 같다고 하고 오늘 문서랑 소스 코드 좀 다시 살펴 봤습니다.


lucene in aciton 에서 퍼온 글)

The start offset is the character position in the original text where the token text begins, and the end offset is the position just after the last character of the token text.


end offset 이 실제 보다 1 큰 이유는 문서에 있습니다.

그런데 왜 이렇게 되었을까를 고민해 보면 내부 처리 방식을  확인해 봐야 합니다.


highlight 기능이기 때문에 이 작업에 필요한 class 파일과 fragment에 대한 처리 로직을 확인 하면 됩니다.

protected String makeFragment( StringBuilder buffer, int[] index, Field[] values, WeightedFragInfo fragInfo,
String[] preTags, String[] postTags, Encoder encoder ){
StringBuilder fragment = new StringBuilder();
final int s = fragInfo.getStartOffset();
int[] modifiedStartOffset = { s };
String src = getFragmentSourceMSO( buffer, index, values, s, fragInfo.getEndOffset(), modifiedStartOffset );
int srcIndex = 0;
for( SubInfo subInfo : fragInfo.getSubInfos() ){
for( Toffs to : subInfo.getTermsOffsets() ){
fragment
.append( encoder.encodeText( src.substring( srcIndex, to.getStartOffset() - modifiedStartOffset[0] ) ) )
.append( getPreTag( preTags, subInfo.getSeqnum() ) )
.append( encoder.encodeText( src.substring( to.getStartOffset() - modifiedStartOffset[0],
to.getEndOffset() - modifiedStartOffset[0] ) ) )
.append( getPostTag( postTags, subInfo.getSeqnum() ) );
srcIndex = to.getEndOffset() - modifiedStartOffset[0];
}
}
fragment.append( encoder.encodeText( src.substring( srcIndex ) ) );
return fragment.toString();
}

코드 보시면 아시겠죠.

기본적으로 String.substring( inclusive begin index, exclusive end index) 을 이용하기 때문에 end offset 값은 1 커야 하는 것입니다.

다른 의미로 보면 그냥 offset 정보와 text 의 length 정보를 한꺼번에 offsets 로 해결하기 좋은 방법으로 봐도 될 것 같습니다.


: