본문 바로가기
코드이그나이터와 php7와 mysql

코드이그나이터4. 네이버 검색 따라잡기-3. 대용량 자료 쾌속 검색!(3)

1. 오토셋 APM 인스톨러 ( apache + php7.2 + mariadb ) 설치 | https://itadventure.tistory.com/93

2. 코드이그나이터 4 ( codeigniter 4 ) 설치 | https://itadventure.tistory.com/95

3. 비주얼 스튜디오 코드 에디터 설치 & 한글 설정 | https://itadventure.tistory.com/96

4. 폴더열기 / 웹페이지 편집(1) | https://itadventure.tistory.com/97

5. 웹페이지 편집(2) | https://itadventure.tistory.com/101

6. 코드이그나이터4의 URL 규칙 | https://itadventure.tistory.com/105

7. php, 네임스페이스 [ namespace ] ?! | https://itadventure.tistory.com/118

8. 코드이그나이터의 네임스페이스, 그리고 모델 | https://itadventure.tistory.com/122

9. 코드이그나이터 뷰의 파라미터 전달 | https://itadventure.tistory.com/147

10. 코드이그나이터 뷰를 나눠 볼까요? | https://itadventure.tistory.com/174

11. MYSQL 이 뭐여? [ 마이에스큐엘은 서류철이다! ] | https://itadventure.tistory.com/175

12. MYSQL 콘솔에 접속해보자! | https://itadventure.tistory.com/178

13. MySql에 넣었다가 꺼냈다가, 뭘? | https://itadventure.tistory.com/265

14. 검색진열대 MYSQL | https://itadventure.tistory.com/267

15. 편집의 왕자 MySQL | https://itadventure.tistory.com/269

16. 코드이그나이터4, MYSQL과 손잡다. | https://itadventure.tistory.com/271

17. MySQL -> 컨트롤러 -> 뷰 트리플 패스! | https://itadventure.tistory.com/272

18. 코드이그나이터! MySQL 에 입력하다! ( insert ) | https://itadventure.tistory.com/273

19. 해킹을 막아라! MySQL인젝션 보안 | https://itadventure.tistory.com/274

20. MySQL과 친해지는 phpmyadmin | https://itadventure.tistory.com/277

21. 코드이그나이터4에서 책 정보를 편집해볼까요? | https://itadventure.tistory.com/280

22. 코드이그나이터4에서 책을 지워봅시다. DELETE! | https://itadventure.tistory.com/282

23. 코드이그나이터4, 페이징 기술 | https://itadventure.tistory.com/285

24. 코드이그나이터4. 검색! | https://itadventure.tistory.com/295

25. 코드이그나이터4. 검색어 자동 추천! | https://itadventure.tistory.com/303

26. 코드이그나이터4. 네이버 검색 따라잡기-1. 대용량 자료 쾌속 검색!(1) | https://itadventure.tistory.com/304

27. 코드이그나이터4. 네이버 검색 따라잡기-2. 대용량 자료 쾌속 검색!(2) | https://itadventure.tistory.com/306

28. 코드이그나이터4. 네이버 검색 따라잡기-3. 대용량 자료 쾌속 검색!(3)


지난 챕터에서 단어사전을 만들고 단어사전을 이용하여 검색속도를 높이는 방법에 대해 살펴보았습니다.

이번 챕터에서는 추가 코드는 없습니다.
다만 설명 진행이 안되었던 부분만 추가로 설명드리도록 하겠습니다.

첫번째 살펴볼 소스는 검색어를 타이핑할때 실행되는 Ajax 코드입니다.
25챕터에서도 타이핑할 때 실행되는 Ajax 소스에 대한 설명은 있었지만 이번의 경우 단어사전을 이용한 Ajax 결과를 불러오는 소스입니다.

우선 booklist5.php 소스에서는 검색어를 타이핑할 때, 다음 자바스크립트가 실행됩니다.

<script>
function findbook()
{
  if($('#searchword').val()=='')
  {
      $("#preview").hide();
      return;
  }
  $.ajax({
    url: "./bookAjax5",
    type: "POST",
    data: {searchword : $('#searchword').val() },
    dataType: "html",
    success: function(data) {
      $("#preview").html(data);
      $("#preview").show();
    }
  });  
}

원래는 bookAjax 가 실행되었었으나, bookAjax5 로 바꾸었지요.
단어사전에서 빠른 결과를 받아오기 위한 목적입니다.

BookAjax5.php 소스는 아래와 같은데요.

<?php
namespace App\Controllers;
use CodeIgniter\Controller;
class BookAjax5 extends Controller
{
    public function index()
    {
        $db = \Config\Database::connect("default", false);
        $searchword=$this->request->getVar("searchword");
        $query = $db->query(
            "SELECT numbers FROM book_search 
            where word like '".addslashes($searchword)."%'");
        $results = $query->getResultArray();
        $numbers=array();
        foreach($results as $result)
            $numbers=array_merge(
                $numbers, 
                explode(",", $result['numbers']));
        sort($numbers);
        $numbers=implode(",", $numbers);
        if($numbers=="")die();
        
        $query = $db->query(
            "SELECT distinct title FROM book where 
            number in ($numbers) limit 10");
        $results = $query->getResultArray();        

        $data = [
            'booklist'=>$results
        ];        
        return view('bookajax', $data);
    }
}

검색페이지 소스와 비슷합니다만, 좀 더 단순합니다.

Database 에 연결하는 부분이야 뭐 어디서나 동일하구요.

$db = \Config\Database::connect("default", false);

넘어오는 변수 searchword 검색어 파라미터를 받아 $searchword 변수에 저장한 다음

$searchword=$this->request->getVar("searchword");

관련 책의 번호 목록 전체를 받아옵니다.

$query = $db->query(
    "SELECT numbers FROM book_search 
    where word like '".addslashes($searchword)."%'");
$results = $query->getResultArray();

책 목록을 조합하여 중복을 지우고 번호순으로 정렬하는 부분은 지난 챕터와 동일하며,

$numbers=array();
foreach($results as $result)
    $numbers=array_merge(
        $numbers, 
        explode(",", $result['numbers']));
sort($numbers);
$numbers=implode(",", $numbers);

다만, 번호 목록이 전혀 없는 경우 아무것도 출력하지 않고 끝나버립니다.

if($numbers=="")die();

만일 책 목록이 있다면 그중에서 최대 10개만 추립니다.

$query = $db->query(
    "SELECT distinct title FROM book where 
    number in ($numbers) limit 10");
$results = $query->getResultArray(); 

그리고 뷰에 넘겨주는 것으로 그 사명이 끝나지요.
bookajax 뷰의 경우 변경된 부분이 없기 때문에 따로 bookajax5 를 만들지 않고 원래 뷰 소스를 그대로 활용하였습니다.

$data = [
    'booklist'=>$results
];        
return view('bookajax', $data);

 

두번째 설명드릴 소스는, 단어사전을 만드는 소스입니다.
내용이 꽤 만만치 않지요? :)

<?php
namespace App\Controllers;
use CodeIgniter\Controller;
class MakeBookWord extends Controller
{
    public function index()
    {        
        ini_set('memory_limit', -1);
        $db = \Config\Database::connect("default", false);
        $query_cnt = $db->query("SELECT count(*) as cnt FROM book");
        $total = $query_cnt->getResultArray()[0]['cnt'];        
        $limit=1000; // 1000 건씩 끊어서 처리
        $word_arr=array();

        $special_chars=array(
            "[", "]", "(", ")", ",", ":", ";", "<", ">",
            "&", "?"
        );
        // 유지할 단어 +-*/.%
        for($step=0;$step<$total;$step+=$limit)
        {
            $results = $db->query("SELECT number, title FROM book order by number limit $step, $limit")->getResultArray();
            foreach($results as $result)
            {
                foreach($special_chars as $char){
                    $result['title']=str_replace($char, " ", $result['title']);
                }
                $explode = array_unique(explode(" ", trim($result['title'])));
                foreach($explode as $word)
                {
                    if(strlen($word)<=1)continue;                    
                    if(!empty($word_arr[$word]))$word_arr[$word].=",";
                    $word_arr[$word].=$result['number'];
                }                    
            }
        }

        echo count($word_arr)."개<br/>";
        ob_flush(); flush();

        $cnt=0;
        $db->query("drop table book_search");
        $db->query("create table book_search (
            word varchar(120),
            numbers text
        )");
        $i=0;
        $db->transStart();
        foreach($word_arr as $word=>$numbers){
            $i++;
            echo "$i: ".$word." / ".$numbers."<br/>";
            $sql="insert into book_search (word, numbers)
            values ('".addslashes($word)."', '".addslashes($numbers)."')";
            $db->query($sql);
            $cnt++;
            if($cnt>=100)
            {
                $db->transComplete();
                $db->transStart();
                $cnt=0;
            }
        }
        $db->transComplete();
        $db->query("ALTER TABLE book_search ADD INDEX(word);");
        
        echo "단어사전 입력이 완료되었습니다.";
    }
}

이 소스는 책의 제목의 단어들을 나누어서 단어사전을 만들고 책번호들을 보관하는데요.
대량의 자료라도 꽤 빠른 속도로 만들어줍니다.

우선 단어 사전 생성도중 메모리 초과 오류가 발생하지 않도록 메모리 제한을 없앱니다.
php 는 하나의 페이지가 작동할 때 기본적으로 메모리 사용량을 제한하기 때문입니다. 

ini_set('memory_limit', -1);

DB 연결 부분은 이제 더 이상 설명은 안 드려도 될듯 합니다 :)

$db = \Config\Database::connect("default", false);

먼저 책이 총 몇 권인인지 알아냅니다. 결과를 $total  변수에 보관해 놓지요

$query_cnt = $db->query("SELECT count(*) as cnt FROM book");
$total = $query_cnt->getResultArray()[0]['cnt'];        

그리고 한번에 1,000건씩 나누어서 읽어오도록 하기 위해 $limit  변수를 선언합니다.

$limit=1000; // 1000 건씩 끊어서 처리

모든 단어를 보관할 배열을 선언하고

$word_arr=array();

공백처럼 취급할 특수문자를 정의합니다.
이 문자들은 문장에서 공백으로 먼저 치환된 다음에 단어사전 생성에 적용될 예정입니다.

$special_chars=array(
    "[", "]", "(", ")", ",", ":", ";", "<", ">",
    "&", "?"
);

다만 아래와 같은 단어는 여기에 포함하지 않았는데 실제로 검색어로 사용될 가능성이 있기 때문입니다.

+ - * / . %

이제 1000개씩 나누어서 실행하도록 for문을 구성한 다음에,

for($step=0;$step<$total;$step+=$limit)
{
      :
}

1000 개씩 끊어서 자료를 불러옵니다. 왜 이렇게 할까요?

그것은 바로 대용량 자료를 용이하게 처리하기 위합니다.
이른바 분할 처리 기법인데 이렇게 해야 자료량이 엄청나게 많을 때 서버컴퓨터에 무리가 없습니다.
이런 처리를 안해주면 서버 메모리가 몇기가 단위로 치솟아 어떤 경우에는 멈춰버릴 수도 있기 때문이지요.

$results = $db->query(
    "SELECT number, title FROM book 
    order by number 
    limit $step, $limit"
)->getResultArray();

최대 1000권의 책목록이 $results 배열변수에 담겨졌습니다.
이제 이 배열변수를 foreach 문으로 반복문을 구성하여 하나씩 $result 변수로 받아옵니다.

foreach($results as $result)
{
    :
}        

책 제목에서 특수문자는 모두 공백으로 바꿔준 다음

foreach($special_chars as $char){
    $result['title']=str_replace($char, " ", $result['title']);
}

책 제목을 공백으로 나누어 각 단어로 쪼개어줍니다.

$explode = array_unique(explode(" ", trim($result['title'])));

쪼갠 각 단어를 $word_arr 배열변수에 담는데, 키를 단어로, 값은 책의 번호를 순차적으로 넣어줍니다

foreach($explode as $word)
{
    if(strlen($word)<=1)continue;                    
    if(!empty($word_arr[$word]))$word_arr[$word].=",";
    $word_arr[$word].=$result['number'];
}                    

모든 처리가 끝나면 $word_arr 배열변수에 모든 책의 각 단어와 책번호가 기록되어 있을 것입니다.
이 것을 테이블에 저장해볼덴데요.
book_search 테이블을 완전히 지웠다가 다시 생성해줍니다.
이 테이블 안에는 찌꺼지가 전혀 남지 않습니다.

$cnt=0;
$db->query("drop table book_search");
$db->query("create table book_search (
    word varchar(120),
    numbers text
)");

화면에 몇번째 자료를 처리중인지 표시할 $i 변수를 선언하여 0을 대입,
그리고 DB 트랜잭션을 시작합니다.

$i=0;
$db->transStart();

보통 php에서 mysql 실행문을 실행할 때, 명령어 하나 단위로 실행하는데요.
DB 트랜잭션은 여러개의 SQL문을 한꺼번에 실행하게 합니다.
그러니 속도가 훨씬 빠릅니다.

트랜잭션은 시작했다면 반드시 끝내야 실행이 되는데요.
트랜잭션을 끝내는 명령은 아래와 같습니다.

$db->transComplete();

트랜잭션을 시작한 후에 foreach 반복문으로 보관한 단어를 화면에 하나씩 뿌려주면서,
단어사전 테이블에 삽입합니다.
하지만 주의하실 부분은 이 SQL문은 트랜잭션을 끝내기 전에는 실행되지 않는다는 점입니다.

foreach($word_arr as $word=>$numbers){
    $i++;
    echo "$i: ".$word." / ".$numbers."<br/>";
    $sql="insert into book_search (word, numbers)
    values ('".addslashes($word)."', '".addslashes($numbers)."')";
    $db->query($sql);

트랜잭션은 너무 길게 실행은 불가합니다. mysql 패킷의 전송량 제한 때문인데요,
단위를 끊어서 실행하는 것이 좋습니다.
$cnt 변수를 1부터 100까지 세먼서 100에 다다를 때마다 트랜잭션을 끝내고 다시 시작하도록 해줍니다.
그러면 안정적으로 트랜잭션이 실행이 되지요.

$cnt++;
if($cnt>=100)
{
    $db->transComplete();
    $db->transStart();
    $cnt=0;
}

$word_arr를 구성하는 반복문이 끝나면 남아 있는 트랜잭션이 있을 수 있으니 한번 더 실행해 줍니다.

}
$db->transComplete();

그리고 마지막으로 단어사전의 인덱스를 구성해 줍니다.

$db->query("ALTER TABLE book_search ADD INDEX(word);");

인덱스는 테이블 생성시 먼저 구성할 수 있는데 굳이 마지막에 구성하는 이유가 있을까요?
물론! 있지요.

연속된 insert 문을 계속 실행하는 경우, 그 당시에는 인덱스가 걸려 있지 않은 것이 더욱 속도가 빠릅니다.
이는 단어를 빨리 검색하도록 insert 문이 실행될 때마다 작동하는 '그 장치'가 없기 때문이지요.
'이진 탐색'을 말하는 것입니다.

특히나 모든 자료가 새로 입력되는 이러한 경우에는 처음에는 인덱스를 잡지 않고 모든 자료를 입력했다가 마지막에 한번에 인덱스를 잡아주는 것이 속도가 훨씬 빠릅니다.

단지 그 이유 때문이지요.

이 것으로 단어사전생성과 타이핑시 실행되는 AJAX에 대해서 알아보았습니다.

이번 시간에는 추가로 보여드린것은 없어 이해에 난해함이 있었을지는 모르겠군요 :)

오늘도 여기까지 읽어주신 분들께 감사드립니다.