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

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

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)


이제 지난 챕터에 이어 책 검색 페이지를 구성해보겠습니다.
바로 소스로 들어가볼까요? 이번에 추가될 소스는 총 3개입니다.

코드이그나이터폴더\app\Controllers\BookList5.php

<?php
namespace App\Controllers;
use CodeIgniter\Controller;
class BookList5 extends Controller
{
    function get_time() { 
        $t=explode(' ',microtime()); 
        return (float)$t[0]+(float)$t[1]; 
    }
    public function index()
    {        
        $start = $this->get_time();
        $pageview=20;
        $page=$this->request->getVar("page");
        $searchword=trim($this->request->getVar("searchword"));

        if($page=="")$page=1;
        $startlimit=($page-1)*$pageview;

        $db = \Config\Database::connect("default", false);
        $where_sql="";        
        if($searchword!=""){
            $query_search=$db->query(
                "SELECT numbers FROM book_search 
                where word like '".addslashes($searchword)."%'");
        
            $results_search = $query_search->getResultArray();
            $numbers = array();
            foreach($results_search as $one)
            {
                $tmp=explode(",", $one['numbers']);
                $numbers = array_merge($numbers, $tmp);
            }
            $total=count($numbers);
            sort($numbers);
            $numbers=implode(",", $numbers);
            if($numbers!="")
                $where_sql="where number in ($numbers)";
            else $where_sql="where 0";
        }
        else 
        {
            $query_count = $db->query(
                "SELECT count(*) FROM book $where_sql");
            $results_count = $query_count->getResultArray();
            $total = $results_count[0]['count(*)'];    
        }

        $totalpage = ceil($total / $pageview);
        $pagestr="{$total}건, {$totalpage}페이지 / ";

        // 페이지 목록
        $page_begin=$page;
        if($page_begin>$totalpage-10)$page_begin=$totalpage-10;
        if($page_begin<1)$page_begin=1;
        $page_end=$page+9;
        if($page_end>$totalpage)$page_end=$totalpage;
        if($page>1)
            $pagestr .= 
                "<a href='?page=1&searchword=$searchword' 
                style='text-decoration:none'>처음</a> ... ";
        for($i=$page_begin;$i<=$page_end;++$i){
            $pagestr .= 
                "<a href='?page=$i&searchword=$searchword' 
                style='text-decoration:none'>";
            if($i==$page)
                $pagestr .= 
                    "<span style='color:red;font-weight:bold'>[";
            $pagestr .= "$i";
            if($i==$page)$pagestr .= "</span>]";
            $pagestr .= "</a> ";
        }

        if($i-1<$totalpage)
            $pagestr .= 
                " ... <a href='?page=$totalpage&searchword=$searchword' 
                style='text-decoration:none'>끝</a>";

        $query = $db->query(
            "SELECT * FROM book $where_sql 
            order by number desc 
            limit $startlimit, $pageview");
    
        $results = $query->getResultArray();
        $data = [
            'title'=>"도서 목록",
            'booklist'=>$results,
            'pagestr'=>$pagestr,
            'searchword'=>$searchword
        ];
        $end = $this->get_time();
        echo "응답속도:".($end - $start)."<br/>";
        return view('booklist5', $data);
    }
}

코드이그나이터폴더\app\Views\booklist5.php

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<style>
    .title_style { font-size:12pt; color:blue}
    .author_style { font-size:9pt; color:gray}
    #preview {
        top:0px;left:0px;
        width:500px;height:200px;
        border:1px solid black;
        background-color:white;
        overflow:auto;
    }
</style>
<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();
    }
  });  
}

function search_input(obj)
{
    $("#searchword").val($(obj).text());
    document.frm.submit();
}
</script>
<u><b><?=$title?></b></u><br/>
<?=$pagestr?>
<form name="frm" method=post action="bookList5">
제목 검색 : <input type=text name=searchword id=searchword value="<?=$searchword?>" onkeyup="findbook()">
<input type=submit value="검색">
<input type=button value="단어사전만들기" onclick="location.href='makeBookWord'">
</form>
<div style="position:relative">
    <div id="preview" style="position:absolute;display:none">

    </div>
</div>
<hr/>
<? foreach ($booklist as $book){?>
<span class=title_style><?= $book['title']?></span>
<span class=author_style>/ <?= $book['author']?></span>
<span class=author_style>/ <a href="bookEdit?number=<?= $book['number']?>">[편집]</a></span>
<span class=author_style>/ <a href="bookDelete?number=<?= $book['number']?>">[삭제]</a></span>
<br/>
<? } ?>
<hr/>
<?=$pagestr?><br/>

코드이그나이터폴더\app\Controllers\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);
    }
}

일부 기능이 개선되었는데요.
검색창에서 타이핑할 때 미리보기 창에 중복 문장이 발생하지 않도록 중복문장은 1번만 노출됩니다.
추가로 반응 시간을 표시하였습니다.

테스트 서적을 10만건 정도 입력해 놓고 테스트해 보았는데,
서버가 아닌 개인 PC라서 가끔 느린 경우를 제외하고는,
한글 2글자 이상 기준으로 대부분 0.01초 ~ 0.03초에 검색이 완료됩니다.
실무 경험상 리눅스 서버에서는 더욱 안정적으로 작동될 것입니다.

소스 변경분에 대해 설명드리겠습니다.
먼저 반응 시간을 보여주기 위해, 컨트롤러에 get_time 이라는 메소드가 추가되었습니다.

function get_time() { 
    $t=explode(" ",microtime());
    return (float)$t[0]+(int)$t[1]; 
}

컨트롤러 안에서 다음과 같이 사용할 수 있는데요.
페이지 시작할 때, 먼저 $start 라는 변수에 $this->get_time() 변수값을 담고 ( 변수명은 자유롭게 주셔도 됩니다 )

$start = $this->get_time();

페이지 끝날 때, $end  라는 변수에 또 담아주시면 됩니다.

$end = $this->get_time();

그리고 나서 view 출력전에, 2개 변수의 차이값을 echo 문으로 찍어주시면,
HTML 소스를 출력하기 바로 전까지의 시간이 화면에 출력되는 것이지요..

echo "응답속도:".($end - $start)."<br/>";

검색어가 있을 경우와 없을 경우 실행되는 부분이 나뉘어졌는데요.
먼저 검색어가 있는 경우, 단어사전에서 검색어에 해당하는 모든 자료를 쿼리하여 $result_search 변수에 가져옵니다.
빠른 속도를 위해 numbers 값만 가져오는 것이지요.

if($searchword!=""){
    $query_search=$db->query(
        "SELECT numbers FROM book_search 
        where word like '".addslashes($searchword)."%'");
    $results_search = $query_search->getResultArray();

검색어로 가져온 numbers 값은 책의 번호값 목록인데요.
만일 '포토'라는 단어로 검색했을때 어떤 책의 이름이 '포토샵으로 만나는 포토디자인'이였다면
'포토샵으로' 라는 단어도 사전에 생겨났을 테고 '포토디자인'이라는 단어도 사전에 생겨났을 겁니다.
그리고 양쪽 단어 모두에 동일한 책 번호 목록이 들어있겠지요.
그래서 겹치는 책번호를 다시 통합해서 한번만 노출되게 해주셔야 합니다.

단어 책번호
포토샵으로 3,9,19
포토디자인 3,12,25 ( 동일한 번호가 양쪽에 겹칠 수 있습니다 )

위의 경우 총 6개지만 3 9 19 12 25 로 중복되는 3을 제거하고 5개로 통합시켜주는 것입니다.
이 부분에 대한 소스는 아래와 같습니다.

$numbers = array();
foreach($results_search as $one)
{
    $tmp=explode(",", $one['numbers']);
    $numbers = array_merge($numbers, $tmp);
}

먼저 $numbers 라는 공백 배열을 선언합니다.

$numbers = array();

그리고 foreach 반복문으로 단어사전이 결과를 하나 하나 받아와서 처리하는 반복문을 구성합니다.

foreach($results_search as $one)
{
    :
}

콤마로 구분된 책번호 문장을 배열로 분리한 다음 배열을 병합해줍니다.
array_merge 명령문은 배열을 병합해서 중복을 제거하고 합쳐주는 명령이지요.

$tmp=explode(",", $one['numbers']);
$numbers = array_merge($numbers, $tmp);

그러면 최종 결과가 $numbers 에 배열로 대입이 됩니다.

$numbers 배열요소
[ 3, 9, 19, 12, 25 ]

이제 페이지수 계산을 위해 총 게시글 수를 구할 차례인데요.
이 경우 select count(*) 어쩌고 쿼리를 돌릴 필요가 없습니다.
그냥 $numbers 배열 요소의 갯수가 전체 게시글 수거든요. 다음과 같이 구해주면 됩니다.

$total=count($numbers);

이제 $numbers 배열 요소를 숫자순으로 정렬하고, 
다시 $numbers 배열을 원래대로 콤마로 구분된 문장으로 합쳐줍니다.
책을 검색할 조건으로 사용하기 위해서이지요.

sort($numbers);
$numbers=implode(",", $numbers);
3,9,12,15,19

책을 검색할 SQL 문 뒤에 붙을 조건문을 구성합니다.
단어 검색결과가 있는 경우는 아래와 같이 $where_sql 변수를 대입하되

 

if($numbers!="")
    $where_sql="where number in ($numbers)";

검색된 단어가 없는 경우에는 다음과 같이 $where_sql 변수를 구성해야 합니다.
그래야 검색 결과가 하나도 나오지 않습니다.

else $where_sql="where 0";

여기까지는 단어 검색을 했던 경우이고,
단어 검색이 아닌 기본 첫페이지의 경우라면
원래 방법대로 select count 명령으로 갯수를 산출하는데 이는 지난번과 동일합니다.

else 
{
     $query_count = $db->query(
        "SELECT count(*) FROM book $where_sql");
     $results_count = $query_count->getResultArray();
     $total = $results_count[0]['count(*)'];    
}

페이지 목록 부분이 약간 바뀌었습니다.
지난번의 경우 모든 페이지가 노출되도록 되어 있어서 만약 10만건 정도의 자료라면 페이지 목록이 화면을 이렇게 장식해줄겁니다.

첫페이지를 꽉 차게 장식해서 정작 책 목록은 보이지도 않는데요.
페이지 처리는 여러가지 기법이 있으나, 단순한 방법을 하나 적용했습니다.

// 페이지 목록
$page_begin=$page;
if($page_begin>$totalpage-10)$page_begin=$totalpage-10;
if($page_begin<1)$page_begin=1;
$page_end=$page+9;
if($page_end>$totalpage)$page_end=$totalpage;
if($page>1)
	$pagestr .= 
		"<a href='?page=1&searchword=$searchword' 
		style='text-decoration:none'>처음</a> ... ";
for($i=$page_begin;$i<=$page_end;++$i){
	$pagestr .= 
		"<a href='?page=$i&searchword=$searchword' 
		style='text-decoration:none'>";
	if($i==$page)
		$pagestr .= 
			"<span style='color:red;font-weight:bold'>[";
	$pagestr .= "$i";
	if($i==$page)$pagestr .= "</span>]";
	$pagestr .= "</a> ";
}

if($i-1<$totalpage)
	$pagestr .= 
		" ... <a href='?page=$totalpage&searchword=$searchword' 
		style='text-decoration:none'>끝</a>";

페이지 목록을 10개씩 표시하되 현재 페이지는 되도록 왼쪽에 위치하도록 하고,
처음페이지와 끝페이지로 가는 링크를 제공하는 것이지요.
페이지는 주 내용이 아니기 때문에 관련 설명은 건너뛰도록 하겠습니다.

이제 본격적인 책 목록 쿼리문을 실행하여 책을 받아오는 부분입니다.

$query = $db->query(
    "SELECT * FROM book $where_sql 
    order by number desc 
    limit $startlimit, $pageview");    
$results = $query->getResultArray();

select 문이 변경되었는데요.
만일 검색조건에 단어사전 목록에서 받아온 최종 $numbers 문장이 3,9,12,15,19 였다면
아래와 같은 SQL 문장을 실행합니다.
number 는 숫자값이고 mysql 에 사전순으로 저장되어 있기 때문에 꽤 빠른 속도로 실행이 되어
검색 결과를 뱉어냅니다.

SELECT * FROM book number in (3,9,12,15,19) order by number desc limit 0, 20

 

다음 챕터에서는 검색창에 타이핑할 때 AJAX 실행 부분과, 단어사전 만드는 부분에 대한 소개가 이어지겠습니다.
이미 소스는 모두 제공된 상태입니다.

오늘도 필요하신 분에게 도움이 되셨나 모르겠습니다.
아무쪼록 건강 유의하시고 오늘도 여기까지 읽어주셔서 감사합니다 :)